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

bis*_*ack 3 javascript reactjs next.js react-suspense

通常,当我返回依赖于获取的数据的组件(我使用 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>
    )


};
Run Code Online (Sandbox Code Playgroud)

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

<Suspense fallback={<div>Loading Message...</div>}>
    <TableComponent />
</Suspense>
Run Code Online (Sandbox Code Playgroud)

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

return (
    <React.Fragment>
        <div>
            {tableEvents[0].title}
        </div>
    </React.Fragment>
)
Run Code Online (Sandbox Code Playgroud)

但它最终无法将组件加载为tableEvents最初为空,并且每次获取都会有所不同,因此它无法具有可预测的键。

React文档展示了一个像这样的简单示例。

有条件地返回渲染也会返回组件,但无法显示悬念回退

if (tableEvents) {
    return (
        <React.Fragment>
            <div>
                {tableEvents[0].event_title}
            </div>
        </React.Fragment>
    )
}
Run Code Online (Sandbox Code Playgroud)

问题

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

She*_*aff 11

长话短说

Suspense被触发,其中一个子级必须throwPromise. 此功能更针对库开发人员,但您仍然可以尝试自己实现一些功能。

伪代码

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

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>
  )
}
Run Code Online (Sandbox Code Playgroud)

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

问题

但现在我们有两个问题:

  • 我们需要能够在没有异步/等待的情况下获得我们承诺的内容,因为在“框架领域”之外的反应中不允许这样做
  • 重新渲染时,fetch实际上会创建一个新的承诺,它将再次抛出,我们将永远循环......

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

解决方案

无需获得承诺状态async

首先,让我们为任何 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
}
Run Code Online (Sandbox Code Playgroud)

通过在每次渲染时调用此函数,我们将能够获取 Promise 的数据,而无需任何async. 然后 ReactSuspense的工作就是在需要时重新渲染。这就是它的作用。

保持持续参考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>
}
Run Code Online (Sandbox Code Playgroud)

更多细节

  • WeakMap在 a 和有关此承诺的一些元数据(状态、返回的数据等)之间进行映射非常理想promise,因为一旦其promise本身在任何地方都没有被引用,元数据就可用于垃圾收集
  • 虽然组件处于“悬而未决”状态(意味着渲染树中从它到下一个Suspense边界的任何组件都会抛出一个承诺),但在每次“尝试”渲染后,它将被 React 卸载。这意味着您不能使用 auseState或 auseRef来保留承诺或其状态。
  • 除非您正在编写一个固执己见的库(例如 tanstack-query),否则几乎不可能有一种普遍有效的存储 Promise 的方法。它完全取决于您的应用程序的行为。它可能就像Map在端点之间建立一个端点并Promise获取该端点一样简单,并且只会随着重新获取、缓存控制、标头、请求参数而变得复杂……这就是为什么我的示例只创建一个简单的承诺一次。

回答问题

使用时Suspense,Suspense 节点内的任何树都不会被渲染,但其中任何树仍会抛出 Promise。如果您需要同时渲染某些内容,这就是fallbackprop 的用途。

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

  • 如果您希望后备与挂起的组件共享一些结构/数据/CSS
  • 如果您想避免加载组件的瀑布式加载,从而阻止大渲染树显示任何内容

  • 这是一个非常有洞察力的答案——我有一条解决方案的路径,并且我一路上学到了很多原因。我真的很感谢你投入的时间 (2认同)