自学内容网 自学内容网

React Query 和 React Context


React Query最佳特性之一是你可以在组件树中的任何位置使用查询:你的 <ProductTable> 组件可以在其需要的地方自带数据获取:

function ProductTable() {
  const productQuery = useProductQuery()

  if (productQuery.data) {
    return <table>...</table>
  }

  if (productQuery.isError) {
    return <ErrorMessage error={productQuery.error} />
  }

  return <SkeletonLoader />
}

它使得 ProductTable组件解耦且独立:它负责读取自己的依赖:产品数据。如果这些数据已经在缓存中,那么很好,我们只需要读取它。如果没有,我们就去获取数据。我们可以在 React Server Components 中看到类似的模式。它们也允许我们在组件内部获取数据。不再需要在有状态和无状态,或智能和哑组件之间进行任意划分。

所以在需要的地方直接在组件内部获取数据是非常有用的。我们可以直接把 ProductTable组件移动到应用程序中的任何位置,它都能正常工作。

自包含性

要让一个组件是自主的,这意味着它必须处理查询数据不可用(尚未加载)的情况,特别是:加载和错误状态。这对我们的 <ProductTable>组件来说并不是什么大问题,因为通常,当它第一次加载时,它实际上会显示 <SkeletonLoader />

但是在很多其他情况下,我们只是想从查询的某些部分读取一些信息,而我们知道查询已经在树的上方使用过。例如,我们可能有一个包含登录用户信息的 userQuery

export const useUserQuery = (id: number) => {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => fetchUserById(id),
  })
}
export const useCurrentUserQuery = () => {
  const id = useCurrentUserId()

  return useUserQuery(id)
}

我们可能会在组件树的早期使用这个查询,以检查登录用户的权限,并且它可能进一步决定我们是否可以看到页面。这是我们希望在页面上的任何地方都能获取的重要信息。

现在在树的更下方,我们可能有一个想要显示 userName的组件,这个信息我们可以通过useCurrentUserQuery 钩子获取:

function UserNameDisplay() {
  const { data } = useCurrentUserQuery()
  return <div>User: {data.userName}</div>
}

TypeScript不会允许我们这样做,因为data可能是未定义的。但我们知道得更好 - 它不可能是未定义的,因为在我们的情况下,如果查询尚未在树的上方初始化,UserNameDisplay就不会被渲染。

我们要让 TypeScript直接使用 data!.userName 吗,因为我们知道它会被定义?我们是要安全起见使用 data?.userName(在这里可能,但在其他情况下可能不容易实现)?我们是否只需添加一个保护:if (!data) return null?还是我们要在调用 useCurrentUserQuery的所有25个地方添加正确的加载和错误处理?

隐含的依赖

我们的问题来自于我们有一个隐含的依赖:一个只存在于我们头脑中的依赖,在我们对应用程序结构的知识中,但它在代码中并不可见。

尽管我们知道可以安全地调用 useCurrentUserQuery而无需检查数据是否未定义,但任何静态分析都无法验证这一点。自己可能在3个月后也不再记得这一点。

最危险的是,现在可能是对的,但将来可能不再正确。我们可能会决定在应用程序的其他地方渲染另一个 UserNameDisplay 实例,在那里我们可能没有用户数据缓存,或者我们可能有条件地缓存用户数据,例如,如果我们之前访问过不同的页面。

这与 <ProductTable> 组件完全相反:它变得容易出错并且难以重构。我们不会期望 UserNameDisplay 组件因为移动了一些看似不相关的组件而崩溃...

明确依赖关系

解决方案当然是让依赖关系明确。而使用 React Context 是最好的方法:

React Context

关于 React Context 存在一些误解,我们先把这些弄清楚:React Context 不是一个状态管理器。当它与useStateuseReducer结合使用时,可能看起来是个不错的状态管理解决方案:React Context是一个依赖注入工具。它允许你定义你的组件需要哪些“东西”,并且由任何父组件负责提供这些信息。

这在概念上与属性传递(prop-drilling)相同,属性传递是通过多个层级传递属性的过程。Context允许你做同样的事情:获取一些值并将其作为属性传递给子组件,只是你可以省去几个中间层级:

使用 context,你只需跳过中间层。在useCurrentUserQuery示例中,它可以帮助我们明确依赖关系:不再在所有需要跳过数据可用性检查的组件中直接读取 useCurrentUserQuery,而是从React Context中读取。而这个context将由实际进行第一次检查的父组件填充:

const CurrentUserContext = React.createContext<User | null>(null)

export const useCurrentUserContext = () => {
  return React.useContext(CurrentUserContext)
}

export const CurrentUserContextProvider = ({
  children,
}: {
  children: React.ReactNode
}) => {
  const currentUserQuery = useCurrentUserQuery()

  if (currentUserQuery.isLoading) {
    return <SkeletonLoader />
  }

  if (currentUserQuery.isError) {
    return <ErrorMessage error={currentUserQuery.error} />
  }

  return (
    <CurrentUserContext.Provider value={currentUserQuery.data}>
      {children}
    </CurrentUserContext.Provider>
  )
}

在这里,我们获取currentUserQuery并将其结果数据放入 React Context中(通过提前消除加载和错误状态)。然后我们可以在子组件中安全地从该 context中读取,例如 UserNameDisplay组件:

function UserNameDisplay() {
  const data = useCurrentUserContext()
  return <div>User: {data.username}</div>
}

这样,我们就明确了隐含的依赖关系(我们知道数据已经在树的上方被获取)。每当有人查看UserNameDisplay时,他们会知道需要从 CurrentUserContextProvider提供数据。这是你在重构时可以记住的事情。如果你改变了 Provider的渲染位置,你也会知道这将影响所有使用该 context 的子组件。这是你无法知道的,当一个组件只是使用查询时——因为查询通常在整个应用程序中是全局的,数据可能存在也可能不存在。

TypeScript

TypeScript 依然不太喜欢这种方式,因为 React Context设计上也适用于没有 Provider的情况,在这种情况下它会给你 Context 的默认值,在我们的例子中是 null。由于我们不希望 useCurrentUserContext在不在 Provider中时工作,我们可以在自定义钩子中添加一个不变量:

export const useCurrentUserContext = () => {
  const currentUser = React.useContext(CurrentUserContext)
  if (!currentUser) {
    throw new Error('CurrentUserContext: No value provided')
  }

  return currentUser
}

这种方法确保了如果我们在错误的位置意外访问 useCurrentUserContext,我们会快速失败并得到一个良好的错误信息。这样,TypeScript将为我们的自定义钩子推断出User类型的值,因此我们可以安全地使用它并访问其属性。

状态同步

你可能会想:这不是“状态同步”吗——将一个值从 React Query复制到另一种状态分发方法?

来源依然是查询。除了 Provider之外,没有其他方法可以改变context值,Provider 将始终反映查询的最新数据。这里没有任何东西被复制,也没有任何东西可能会不同步。从 React Query作为属性传递数据给子组件也不是“状态同步”,而且由于context类似于属性传递,这也不是“状态同步”。

请求瀑布

没有什么是没有缺点的,这种技术也不例外。具体来说,它可能会产生网络瀑布,因为你的组件树会在 Provider处停止渲染,因此子组件不会被渲染,无法发出网络请求,即使它们是无关的。

考虑这种方法用于子树中必需的数据:用户信息是一个很好的例子,因为如果没有这些数据我们可能不知道该渲染什么。

Suspense

谈到 Suspense:是的,你可以用React Suspense实现类似的架构,并且它也有同样的缺点:潜在的请求瀑布,
一个问题是,在当前的主要版本(v4)中,对查询使用 suspense: true 不会对 data 进行类型收窄,因为还有其他方式可以禁用查询并使其不运行。

然而,自 v5 起,有一个显式的useSuspenseQuery钩子,在组件渲染时数据被保证是已定义的。这样,我们可以这样做:

function UserNameDisplay() {
  const { data } = useSuspenseQuery(...)
  return <div>User: {data.username}</div>
}

这样 TypeScript就会对它满意。🎉



喜欢的朋友记得点赞、收藏、关注哦!!!


原文地址:https://blog.csdn.net/qq_24428851/article/details/142902336

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!