gpt4 book ai didi

javascript - 有条件地返回 React 组件以满足 Suspense 回退

转载 作者:行者123 更新时间:2023-12-02 17:57:44 26 4
gpt4 key购买 nike

通常当我返回一个依赖于获取数据的组件(我使用的是 nextjs 13)时,我有条件地渲染元素以确保值可用:

表格组件:

export const Table = ({ ...props }) => {

const [tableEvents, setTableEvents] = useState(null);

useEffect(() => {
fetchAndSetTableEvents();
}, []);


async function fetchAndSetTableEvents() {
const fetchedEvents = await fetch(* url and params here *)
if (fetchedEvents && fetchedEvents) {
setTableEvents(fetchedEvents);
} else {
setTableEvents(null);
}
}

return (
<React.Fragment>
<div>
{tableEvents ? tableEvents[0].title : null}
</div>
</React.Fragment>
)


};

如果我尝试使用 Suspense 从父组件加载 TableComponent,它会加载但不会在加载前显示回退:

<Suspense fallback={<div>Loading Message...</div>}>
<TableComponent />
</Suspense>

但是,如果我删除 TableComponent 中的条件呈现并仅指定变量,则回退会在尝试加载组件时正确显示:

return (
<React.Fragment>
<div>
{tableEvents[0].title}
</div>
</React.Fragment>
)

但它最终无法加载组件,因为 tableEvents 最初为 null 并且会在每次提取时发生变化,因此它不能具有可预测的键。

React docs对于 Suspense,只显示一个像这样的简单示例。

有条件地返回render也返回组件ok但是没有显示suspense fallback

if (tableEvents) {
return (
<React.Fragment>
<div>
{tableEvents[0].event_title}
</div>
</React.Fragment>
)
}

问题

我如何在组件中获取和返回值,这些值可能存在也可能不存在,满足加载时显示的 Suspense 回退标准。我假设它以一种我正在阻止但找不到解决方法的方式依赖于 Promise。

最佳答案

长话短说

要触发Suspense,其中一个 child 必须throw一个Promise。此功能更多地针对库开发人员,但您仍然可以尝试自己实现一些功能。

伪代码

基本思想很简单,这是伪代码

function ComponentWithLoad() {
const promise = fetch('/url') // create a promise

if (promise.pending) { // as long as it's not resolved
throw promise // throw the promise
}

// otherwise, promise is resolved, it's a normal component
return (
<p>{promise.data}</p>
)
}

Suspense 边界被抛出 Promise 时,它将等待它,并在 promise 解决时重新渲染组件。就这样。

问题

除了现在我们有 2 个问题:

  • 我们需要能够在没有 async/await 的情况下获得我们 promise 的内容,因为在“Framework Land”之外的 react 中不允许这样做
  • 在重新渲染时,fetch 实际上会创建一个 promise,它将再次被抛出,我们将永远循环...

这两个问题的解决方案是找到一种方法将 promise 存储在 Suspense 边界之外(并且很可能完全在 react 之外)。

解决方案

不用async获取promise状态

首先,让我们围绕任何允许我们获取其状态(待处理、已解决、已拒绝)或已解决数据的 promise 编写一个包装器。

const promises = new WeakMap()
function wrapPromise(promise) {
const meta = promises.get(promise) || {}

// for any new promise
if (!meta.status) {
meta.status = 'pending' // set it as pending
promise.then((data) => { // when resolved, store the data
meta.status = 'resolved'
meta.data = data
})
promise.catch((error) => { // when rejected store the error
meta.status = 'rejected'
meta.error = error
})
promises.set(promise, meta)
}

if (meta.status === 'pending') { // if still pending, throw promise to Suspense
throw promise
}
if (meta.status === 'error') { // if error, throw error to ErrorBoundary
throw new Error(meta.error)
}

return meta.data // otherwise, return resolved data
}

通过在每次渲染时调用此函数,我们将能够在没有任何 async 的情况下获取 promise 的数据。然后 React Suspense 会在需要时重新渲染。这就是它的作用。

维护对 Promise

的常量引用

然后我们只需要将我们的 promise 存储在 Suspense 边界之外。最简单的例子是在父级中声明它,但理想的解决方案(避免在父级本身重新呈现时创建新的 promise )将它存储在 react 本身之外。

export default function App() {

// create a promise *outside* of the Suspense boundary
const promise = fetch('/url').then(r => r.json())

// Suspense will try to render its children, if rendering throws a promise, it'll try again when that promise resolves
return (
<Suspense fallback={<div>Loading...</div>}>
{/* we pass the promise to our suspended component so it's always the same `Promise` every time it re-renders */}
<ComponentWithLoad promise={promise} />
</Suspense>
)
}

function ComponentWithLoad({promise}) {
// using the wrapper we declared above, it will
// - throw a Promise if it's still pending
// - return synchronously the result of our promise otherwise
const data = wrapPromise(promise)

// we now have access to our fetched data without ever using `async`
return <p>{data}</p>
}

更多细节

  • WeakMap 非常适合在 promise 和关于此 promise 的一些元数据(状态、返回数据等)之间进行映射,因为一旦 promise 本身在任何地方都没有被引用,元数据可用于垃圾收集
  • 当一个组件处于“暂停状态”时(意味着从它到下一个 Suspense 边界的渲染树中的任何组件都会抛出一个 promise ),它会在每次“尝试”后被 react 卸载渲染。这意味着您不能使用 useStateuseRef 来保存 promise 或其状态。
  • 除非您正在编写一个自以为是的库(例如 tanstack-query),否则几乎不可能有一种普遍有效的方式来存储 promise 。这完全取决于您的应用程序的行为。它可能就像在端点之间有一个 Map 和获取该端点的 Promise 一样简单,并且只会随着重新获取、缓存控制、 header 、请求参数而变得更加复杂... 这就是为什么我的示例只创建一次简单的 promise 。

问题的答案

当使用 Suspense 时,Suspense 节点内的任何树都不会被渲染,同时它仍然抛出一个 Promise。如果您需要同时渲染某些东西,这就是 fallback 属性的用途。

它确实需要我们改变我们对组件分割的思考方式

  • 如果你想让你的回退与挂起的组件共享一些结构/数据/css
  • 如果您想避免加载组件的瀑布式加载,从而阻止大型渲染树根本无法显示任何内容

关于javascript - 有条件地返回 React 组件以满足 Suspense 回退,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75288420/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com