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

3
通常情况下,当我返回一个依赖于获取的数据的组件时(我正在使用nextjs 13),我会有条件地渲染元素以确保值可用:
TableComponent:
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,它会加载但在加载完成之前不显示fallback。
<Suspense fallback={<div>Loading Message...</div>}>
    <TableComponent />
</Suspense>

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

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

但它最终无法加载组件,因为tableEvents最初为null,并且每次获取都会有所变化,因此无法拥有可预测的键。

React文档中关于暂停的示例非常简单。

条件返回渲染也会成功返回组件,但无法显示悬挂回退。

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

问题

如何在组件中获取并返回值,这些值可能存在也可能不存在,并且满足Suspense回退的条件以在加载时显示。我假设它依赖于Promise,但我却无法找到解决方法。

1个回答

10

简而言之

要触发Suspense,其中一个子元素必须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解决时重新渲染组件。就是这样。

问题

除此之外,我们现在有两个问题:

  • 我们需要能够在没有async/await的情况下获取我们promise的内容,因为在“框架区”以外的react中不允许使用它
  • 重新渲染后,fetch实际上会创建一个新的 promise,它将再次被抛出,我们将永远循环...

解决这两个问题的方法是找到一种方法来存储Suspense边界之外的promise(并且很可能是在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
}

每次渲染时调用此函数,我们可以在不使用任何异步操作的情况下获取Promise的数据。然后,React Suspense将会在需要时重新渲染,这就是它的作用。
保持对Promise的恒定引用
然后我们只需要在Suspense边界之外存储我们的Promise。最简单的示例是在父级中声明它,但理想的解决方案(避免在父级本身重新渲染时创建新的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的方法。它完全取决于您的应用程序行为。它可能只是在端点和获取该端点的Promise之间创建一个简单的Map,并且随着重新获取、缓存控制、标头、请求参数等的增加而变得更加复杂。这就是为什么我的示例只创建了一个简单的promise

问题的答案

使用 Suspense 时,只要其中任何部分仍然抛出 Promise,Suspense 节点内的所有树都不会被渲染。如果您需要在此期间渲染一些内容,那就是 fallback 属性的作用。

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

  • 如果您希望回退共享一些结构/数据/CSS与暂停的组件
  • 如果您想避免加载组件的瀑布式阻止大型渲染树显示任何内容

2
这是一个非常有见地的答案 - 我有了一条通往解决方案的路径,并且在这个过程中学到了很多原因。我非常感谢您所付出的时间。 - biscuitstack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接