ssr中使用react-query踩坑记录 基础版

164 阅读4分钟

ssr基础版

import { dehydrate, HydrationBoundary, QueryClient, queryOptions, useQuery } from '@tanstack/react-query'
import { data as reactRouterData, useLoaderData } from 'react-router'
import type { Route } from './+types/login'
import { queryClient } from '@/api'
async function getPosts() {
  console.log('开始获取帖子数据...')
  // 模拟网络请求延迟
  await new Promise((resolve) => setTimeout(resolve, 2000))
  console.log('帖子数据获取完成!')
  // 返回模拟数据
  return [
    {
      id: '1',
      title: '如何在 React 中优化性能',
      slug: 'optimize-react-performance',
      summary: '本文将介绍 React 应用性能优化的多种方法,从组件渲染到状态管理。',
      content:
        '随着 React 应用规模的增长,性能问题逐渐显现。本文将深入探讨如何通过 useMemo、useCallback、React.memo 等方式减少不必要的渲染,如何通过状态设计提升应用响应速度。',
      author: {
        id: 'auth1',
        name: '张三',
        avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
      },
      tags: ['React', '性能优化', '前端开发'],
      publishedAt: '2023-09-15T08:24:00Z',
      readTime: 8,
      likes: 124,
      comments: 18,
    },
  ]
}

export async function loader() {
  console.log('loader111')
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return { dehydratedState: dehydrate(queryClient) }
  // return {}
}

export async function clientLoader({ loaderData }: Route.ClientLoaderArgs) {
  console.log('clientLoader111')
  // const data = await queryClient.ensureQueryData(contactListQuery())
  // const data =
  //   queryClient.getQueryData(['posts']) ??
  //   (await queryClient.fetchQuery({
  //     queryKey: ['posts'],
  //     queryFn: getPosts,
  //   }))
  await queryClient.invalidateQueries({ queryKey: ['posts'] })
  // 不使用hooks,仅返回数据
  return { dehydratedState: {} }
}

export default function PostsRoute({ loaderData }: Route.ComponentProps) {
  const data = useLoaderData<typeof loader>()

  const postsQuery = useQuery({ queryKey: ['posts'], queryFn: getPosts })
  return (
    <HydrationBoundary state={data.dehydratedState}>
      <div>
        <h4>{JSON.stringify(postsQuery)}</h4>
      </div>
    </HydrationBoundary>
  )
}

User

什么是水合 请画出当前react-query水合的流程图


Assistant

什么是"水合"(Hydration)

在SSR(服务器端渲染)上下文中,"水合"是一个比喻性术语,指的是将"干燥"的静态HTML内容转变为"活跃"的交互式客户端应用的过程。就像干燥的海绵吸收水分变得充满活力一样,静态HTML通过JavaScript"吸收"交互能力变成完全功能性的React应用。

对于React Query来说,水合特指将服务器端预取并序列化的查询数据在客户端重新"激活"的过程,使客户端QueryClient能够使用这些数据而不需要重新发起请求。

React Query水合流程图

┌───────────────────────────────┐           ┌────────────────────────────────┐
│         服务器端              │           │            客户端              │
└───────────────────────────────┘           └────────────────────────────────┘
          │                                              │
          ▼                                              │
┌───────────────────────────────┐                        │
│ 1. 创建QueryClient实例        │                        │
└───────────────────────────────┘                        │
          │                                              │
          ▼                                              │
┌───────────────────────────────┐                        │
│ 2. 使用prefetchQuery预获取数据 │                        │
└───────────────────────────────┘                        │
          │                                              │
          ▼                                              │
┌───────────────────────────────┐                        │
│ 3. 使用dehydrate()序列化      │                        │
│    QueryClient缓存            │                        │
└───────────────────────────────┘                        │
          │                                              │
          ▼                                              │
┌───────────────────────────────┐                        │
│ 4. 将序列化数据作为props传递   │                        │
│    或嵌入HTML                 │                        │
└───────────────────────────────┘                        │
          │                                              │
          └──────────────────────────────────────────────┘
                                  │
                                  ▼
                      ┌────────────────────────────────┐
                      │ 5. 客户端接收HTML和序列化数据   │
                      └────────────────────────────────┘
                                  │
                                  ▼
                      ┌────────────────────────────────┐
                      │ 6. 客户端创建QueryClient实例    │
                      └────────────────────────────────┘
                                  │
                                  ▼
                      ┌────────────────────────────────┐
                      │ 7. HydrationBoundary组件接收   │
                      │    序列化状态(state属性)        │
                      └────────────────────────────────┘
                                  │
                                  ▼
                      ┌────────────────────────────────┐
                      │ 8. 内部调用hydrate()将数据     │
                      │    注入QueryClient缓存         │
                      └────────────────────────────────┘
                                  │
                                  ▼
                      ┌────────────────────────────────┐
                      │ 9. 组件内useQuery直接从缓存    │
                      │    获取数据,无需重新请求       │
                      └────────────────────────────────┘
                                  │
                                  ▼
                      ┌────────────────────────────────┐
                      │ 10. 根据staleTime设置,数据    │
                      │     最终在后台重新验证        │
                      └────────────────────────────────┘

在示例代码中的具体应用

以提供的代码为例:

  1. 服务器端:

    export async function loader() {
      const queryClient = new QueryClient()
      await queryClient.prefetchQuery({
        queryKey: ['posts'],
        queryFn: getPosts,
      })
      return { dehydratedState: dehydrate(queryClient) }
    }
    
  2. 客户端:

    export default function PostsRoute() {
      const data = useLoaderData<typeof loader>()
      const postsQuery = useQuery({ queryKey: ['posts'], queryFn: getPosts })
      
      return (
        <HydrationBoundary state={data.dehydratedState}>
          <div>
            <h4>{JSON.stringify(postsQuery)}</h4>
          </div>
        </HydrationBoundary>
      )
    }
    

这个过程实现了无缝的数据传输,确保客户端初始渲染时已有数据可用,避免了不必要的网络请求和加载状态。


User

疑问: 为什么服务端每次都要请求新的react-query实例呢? 服务端共享一个实例会怎样


Assistant

关于服务端 React Query 实例的疑问解析

为什么服务端每次都要创建新的 QueryClient 实例?

在服务端渲染(SSR)环境中,为每个请求创建新的 QueryClient 实例是一种安全且必要的做法,有以下几个关键原因:

1. 请求隔离

┌─────────────────┐     ┌─────────────────┐
│  用户A的请求     │     │  用户B的请求     │
└─────────────────┘     └─────────────────┘
        │                       │
        ▼                       ▼
┌─────────────────┐     ┌─────────────────┐
│ QueryClient A   │     │ QueryClient B   │
└─────────────────┘     └─────────────────┘

每个用户请求应该有其完全独立的数据环境,不受其他请求影响。

2. 数据安全与隐私

若共享实例:

┌─────────────────┐     ┌─────────────────┐
│  用户A的请求     │     │  用户B的请求     │
└─────────────────┘     └─────────────────┘
        │                       │
        └──────────┬────────────┘
                   ▼
          ┌─────────────────┐
          │ 共享QueryClient │ ← 数据可能交叉污染!
          └─────────────────┘

用户A可能会看到用户B的数据,造成严重的隐私泄露问题。

3. 内存管理

每个请求完成后,其相关的 QueryClient 实例和缓存数据应该被垃圾回收。共享实例会导致内存持续增长。

4. 并发请求处理

时间轴 →
请求1: ──[修改QueryClient缓存]─────────────────────────→
请求2: ────────[读取被修改的缓存]───[返回错误数据]────────→

在高并发环境下,共享实例会导致竞态条件,一个请求可能修改另一个请求正在使用的缓存。

5. 请求状态独立性

每个请求的查询状态(加载中、错误、成功)应该是独立的,不应该受其他请求的影响。

如果共享一个实例会发生什么?

  1. 数据交叉污染:用户A可能看到用户B的私人数据
  2. 缓存不一致:并发请求之间互相覆盖缓存
  3. 内存泄漏:随着时间推移,数据不断累积而不被清理
  4. 安全隐患:敏感数据可能在不同用户间泄露
  5. 状态混乱:不同请求的加载状态、错误状态相互影响

在代码中的体现

export async function loader() {
  // 为每个请求创建新的QueryClient实例
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  // 序列化并返回此请求特有的状态
  return { dehydratedState: dehydrate(queryClient) }
}

这种设计确保了每个用户请求都有其独立且安全的数据环境,遵循了Web应用的基本隔离原则。