使用 react-query 让状态管理更加高效优雅

什么是 react-query

React Query 是一个基于 React 的轻量级数据获取和状态管理库,其主要关注点在于客户端如何更好地管理服务器端状态。与传统的状态管理库(如 Redux 和 MobX)相比,它专注于处理服务器状态,简化了与后端数据交互的逻辑。

React Query 通过提供 useQuery、useMutation 等 hooks,使得开发者能够轻松地获取、更新、删除服务器端数据。此外,它还内置了数据缓存、自动更新、重试等功能,进一步优化了客户端与服务器端状态同步的体验。

因此,React Query 的核心价值在于帮助开发者更优雅地管理客户端与服务器端状态的交互,提升前端开发效率。

客户端应用状态

  1. 客户端状态 Client State:多数用于控制客户端的 UI 展示,储存在于客户端。

  2. 服务端状态 Server State:客户端通过异步请求获得的数据,储存在于服务端.

服务端状态有以下特点

  1. 存储在远端

  2. 需要异步 API 来查询和更新

  3. 数据不同步

React Query 还针对下列常见需求给出了自己的解决方案

1. 缓存

1import { useQuery } from 'react-query'
2
3function App() {
4  const { isLoading, error, data } = useQuery('todo', () =>
5    fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json()))
6
7  if (isLoading)
8    return <div>Loading...</div>
9
10  if (error) {
11    return (
12      <div>
13        Error:
14        {error.message}
15      </div>
16    )
17  }
18
19  return (
20    <div>
21      <h1>Todo</h1>
22      <div>{data.title}</div>
23    </div>
24  )
25}
26
27export default App

2. 将对同一数据的多个请求简化为一个请求

1import { useQuery } from 'react-query'
2
3function App() {
4  const { isLoading, error, data } = useQuery('todos', () =>
5    fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()))
6
7  if (isLoading)
8    return <div>Loading...</div>
9
10  if (error) {
11    return (
12      <div>
13        Error:
14        {error.message}
15      </div>
16    )
17  }
18
19  return (
20    <ul>
21      {data.map(todo => (
22        <li key={todo.id}>{todo.title}</li>
23      ))}
24    </ul>
25  )
26}
27
28export default App

3. 在后台更新”过期”数据

1import { useQuery } from 'react-query'
2
3function App() {
4  const { isLoading, error, data } = useQuery(
5    'todos',
6    () => fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()),
7    {
8      refetchOnWindowFocus: false,
9      staleTime: 10000, // 10 秒后数据过期,但仍会在后台更新
10    }
11  )
12
13  if (isLoading)
14    return <div>Loading...</div>
15
16  if (error) {
17    return (
18      <div>
19        Error:
20        {error.message}
21      </div>
22    )
23  }
24
25  return (
26    <ul>
27      {data.map(todo => (
28        <li key={todo.id}>{todo.title}</li>
29      ))}
30    </ul>
31  )
32}
33
34export default App

4. 知道数据何时”过期”

1import { useQuery } from 'react-query'
2
3function App() {
4  const { isLoading, error, data, isStale } = useQuery(
5    'todos',
6    () => fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()),
7    {
8      staleTime: 10000, // 10 秒后数据过期
9    }
10  )
11
12  if (isLoading)
13    return <div>Loading...</div>
14
15  if (error) {
16    return (
17      <div>
18        Error:
19        {error.message}
20      </div>
21    )
22  }
23
24  return (
25    <div>
26      <ul>
27        {data.map(todo => (
28          <li key={todo.id}>{todo.title}</li>
29        ))}
30      </ul>
31      {isStale && <div>Data is stale</div>}
32    </div>
33  )
34}
35
36export default App

5. 尽可能快地反映数据的更新

1import { useQuery, useMutation } from 'react-query';
2
3interface TodoProps {
4  id: number;
5  title: string;
6  completed: boolean;
7}
8
9const fetchTodos = async (): Promise<TodoProps[]> =>
10  fetch('https://jsonplaceholder.typicode.com/todos').then((res) => res.json());
11
12const updateTodo = async (todo: TodoProps): Promise<TodoProps> =>
13  fetch(`https://jsonplaceholder.typicode.com/todos/${todo.id}`, {
14    method: 'PUT',
15    headers: {
16      'Content-Type': 'application/json',
17    },
18    body: JSON.stringify(todo),
19  }).then((res) => res.json());
20
21function App() {
22  const { isLoading, error, data } = useQuery('todos', fetchTodos);
23
24  const queryClient = useQueryClient();
25  const { mutate } = useMutation(updateTodo, {
26    onSuccess: (data) => {
27      queryClient.setQueryData('todos', (old) => old.map((todo: TodoProps) => (todo.id === data.id ? data : todo)));
28    },
29  });
30
31  const handleToggleComplete = (todo: TodoProps) => {
32    mutate({ ...todo, completed: !todo.completed });
33  };
34
35  if (isLoading) {
36    return <div>Loading...</div>;
37  }
38
39  if (error) {
40    return <div>Error: {error.message}</div>;
41  }
42
43  return (
44    <ul>
45      {data.map((todo: TodoProps) => (
46        <li key={todo.id}>
47          {todo.title} <input type="checkbox" checked={todo.completed} onChange={() => handleToggleComplete(todo)} />
48        </li>
49      ))}
50    </ul>
51  );
52}
53
54export default App;

6. 性能优化,如分页和懒加载数据

1import { useInfiniteQuery } from 'react-query';
2
3interface TodoProps {
4  id: number;
5  title: string;
6  completed: boolean;
7}
8
9const fetchTodos = async ({ pageParam = 0 }) =>
10  fetch(`https://jsonplaceholder.typicode.com/todos?_page=${pageParam}`).then((res) => res.json());
11
12function Todos() {
13  const { data, error, isLoading, isFetching, fetchNextPage } = useInfiniteQuery('todos', fetchTodos, {
14    getNextPageParam: (lastPage) => {
15      const nextPage = lastPage.length > 0 ? lastPage[lastPage.length - 1].id : null;
16      return nextPage ? nextPage + 1 : null;
17    },
18  });
19
20  if (isLoading) {
21    return <div>Loading...</div>;
22  }
23
24  if (error) {
25    return <div>Error: {error.message}</div>;
26  }
27
28  return (
29    <>
30      {data.pages.map((page) => (
31        <ul key={page[0].id}>
32          {page.map((todo: TodoProps) => (
33            <li key={todo.id}>
34              {todo.title} {todo.completed && '✓'}
35            </li>
36          ))}
37        </ul>
38      ))}
39      {isFetching ? <div>Fetching more...</div> : null}
40      <button onClick={() => fetchNextPage()} disabled={!data.hasNextPage}>
41        Load more
42      </button>
43    </>
44  );
45}
46
47export default Todos;

7. 管理内存

1import { useQuery } from 'react-query'
2
3function App() {
4  const { isLoading, error, data } = useQuery('todos', () =>
5    fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()))
6
7  if (isLoading)
8    return <div>Loading...</div>
9
10  if (error) {
11    return (
12      <div>
13        Error:
14        {error.message}
15      </div>
16    )
17  }
18
19  return (
20    <ul>
21      {data.map(todo => (
22        <li key={todo.id}>{todo.title}</li>
23      ))}
24    </ul>
25  )
26}
27
28export default App

8. 共享数据

1// users.ts
2import { useQuery } from 'react-query';
3
4interface UserProps {
5  id: number;
6  name: string;
7}
8
9const fetchUsers = async (): Promise<UserProps[]> =>
10  fetch('https://jsonplaceholder.typicode.com/users').then((res) => res.json());
11
12export function useUsers() {
13  return useQuery('users', fetchUsers);
14}
15
16// todos.ts
17import { useQuery } from 'react-query';
18
19interface TodoProps {
20  id: number;
21  title: string;
22  completed: boolean;
23}
24
25const fetchTodos = async (): Promise<TodoProps[]> =>
26  fetch('https://jsonplaceholder.typicode.com/todos').then((res) => res.json());
27
28export function useTodos() {
29  return useQuery('todos', fetchTodos);
30}
31
32// app.tsx
33import { useUsers } from './users';
34import { useTodos } from './todos';
35
36function App() {
37  const { data: users } = useUsers();
38  const { isLoading, error, data: todos } = useTodos();
39
40  if (isLoading) {
41    return <div>Loading...</div>;
42  }
43
44  if (error) {
45    return <div>Error: {error.message}</div>;
46  }
47
48  return (
49    <>
50      <h1>Todos</h1>
51      <ul>
52        {todos.map((todo) => (
53          <li key={todo.id}>
54            {todo.title} {todo.completed && '✓'} ({users.find((user) => user.id === todo.userId)?.name})
55          </li>
56        ))}
57      </ul>
58    </>
59  );
60}
61
62export default App;

使用 react-query

1. 首先,需要安装 React Query:

1npm install react-query

安装完成后,在项目的根组件中引入 QueryClientQueryClientProvider

1import { QueryClient, QueryClientProvider } from 'react-query'
2
3const queryClient = new QueryClient()
4
5function App() {
6  return <QueryClientProvider client={queryClient}>{/* 应用的其他部分 */}</QueryClientProvider>
7}
8
9export default App

2. 使用 useQuery 获取数据

React Query 提供了一个名为 useQuery 的 hook,可以用于获取远程数据。这是一个简单的示例:

1import { useQuery } from 'react-query'
2import axios from 'axios'
3
4async function fetchUsers() {
5  const response = await axios.get('https://api.example.com/users')
6  return response.data
7}
8
9function Users() {
10  const { data, isLoading, error } = useQuery('users', fetchUsers)
11
12  if (isLoading)
13    return <div>加载中...</div>
14
15  if (error) {
16    return (
17      <div>
18        发生错误:
19        {error.message}
20      </div>
21    )
22  }
23
24  return (
25    <ul>
26      {data.map(user => (
27        <li key={user.id}>{user.name}</li>
28      ))}
29    </ul>
30  )
31}
32
33export default Users

3. 优雅地处理错误和重试

React Query 默认会在请求失败时尝试重试 3 次。你还可以自定义重试次数和重试间隔,例如:

1const { data, isLoading, error } = useQuery('users', fetchUsers, {
2  retry: 5,
3  retryDelay: attempt => attempt * 1000,
4})

4. 缓存和自动更新

React Query 默认会缓存数据,减少不必要的请求。当组件卸载后,数据仍然保留在缓存中。当再次使用相同的 key 查询时,React Query 会直接使用缓存中的数据。同时,React Query 还可以在后台自动更新数据,例如:

1const { data, isLoading, error } = useQuery('users', fetchUsers, {
2  refetchOnWindowFocus: true,
3})

5. 使用 useMutation 发送数据

React Query 还提供了 useMutation hook,用于处理数据的更改(如添加、修改、删除)。这是一个简单的示例:

1import { useMutation } from 'react-query'
2import axios from 'axios'
3async function addUser(newUser) {
4  const response = await axios.post('https://api.example.com/users', newUser)
5  return response.data
6}
7
8function CreateUser() {
9  const mutation = useMutation(addUser, {
10    onSuccess: () => {
11      // 通知用户添加成功
12      alert('用户添加成功!')
13    },
14    onError: () => {
15      // 通知用户添加失败
16      alert('用户添加失败,请重试。')
17    },
18  })
19
20  const handleSubmit = (e) => {
21    e.preventDefault()
22    const newUser = {
23      name: e.target.name.value,
24    }
25    mutation.mutate(newUser)
26  }
27
28  return (
29    <form onSubmit={handleSubmit}>
30      <input type="text" name="name" placeholder="请输入用户名" />
31      <button type="submit">添加用户</button>
32    </form>
33  )
34}
35
36export default CreateUser

在上面的示例中,useMutation hook 用于处理添加用户的操作。当添加成功时,会显示成功提示;如果添加失败,则显示失败提示。

6. 使用 QueryClient 无缝整合

使用 QueryClient 可以让你更好地控制 React Query 的行为。例如,你可以在添加用户成功后,使用户列表的缓存失效,以便立即获取更新后的数据:

1import { useQueryClient } from 'react-query'
2
3function CreateUser() {
4  const queryClient = useQueryClient()
5
6  const mutation = useMutation(addUser, {
7    onSuccess: () => {
8      // 使用户列表缓存失效
9      queryClient.invalidateQueries('users')
10      // 通知用户添加成功
11      alert('用户添加成功!')
12    },
13    onError: () => {
14      // 通知用户添加失败
15      alert('用户添加失败,请重试。')
16    },
17  })
18
19  // ... 其他代码
20}

总结 React Query 是一个强大且灵活的状态管理库,可以让你的项目状态管理变得更加高效优雅。通过使用 React Query 提供的 useQueryuseMutation 等 hooks,可以轻松地处理服务器状态,同时享受缓存、重试和自动更新等功能。如果你在寻找一个简单易用且功能强大的状态管理库,React Query 是一个值得尝试的选择。

QueryClient

`QueryClient 是 React Query 的核心类,它负责管理查询和突变的缓存、配置以及其他内部状态。你可以将其视为一个全局对象,它在整个应用程序中存储并管理所有查询和突变的状态。

创建一个 QueryClient 实例的方法如下:

1import { QueryClient } from 'react-query'
2const queryClient = new QueryClient()

在创建 QueryClient 时,你还可以传入配置选项来自定义其行为。例如:

1const queryClient = new QueryClient({
2  defaultOptions: {
3    queries: {
4      retry: 3, // 设置全局默认的重试次数
5      cacheTime: 1000 * 60 * 5, // 缓存数据的时长(毫秒)
6    },
7  },
8})

QueryClientProvider

QueryClientProvider 是一个 React 组件,它的作用是将创建好的 QueryClient 实例传递给应用程序中的其他组件。你可以将它视为 React Query 的上下文提供者,它使得 React Query 可以在整个应用程序范围内工作。使用 QueryClientProvider 的方法如下:

1import { QueryClient, QueryClientProvider } from 'react-query'
2
3const queryClient = new QueryClient()
4
5function App() {
6  return <QueryClientProvider client={queryClient}>{/* 应用的其他部分 */}</QueryClientProvider>
7}
8
9export default App

在项目的根组件中引入 QueryClientProvider 并传入 QueryClient 实例,这样你就可以在应用的任何地方使用 React Query 提供的 hooks,例如 useQuery 和 useMutation 等。

QueryClient 负责管理和配置 React Query 的内部状态,而 QueryClientProvider 则负责将 QueryClient 实例传递给整个应用程序,使得其他组件可以方便地使用 React Query 的功能。在使用 React Query 时,这两个组件是不可或缺的。

useMutation 里 如何将实时状态传递出去

useMutation 提供了一个名为 onMutate 的配置选项,可以在 mutation 开始之前执行。你可以使用此选项捕获 mutation 的实时状态,并将其传递给外部组件。例如,我们在添加用户的示例中增加一个实时状态传递功能。首先,在 CreateUser 组件中定义一个名为 onStatusChange 的回调函数,然后将此回调函数作为 prop 传递给子组件:

1function CreateUser({ onStatusChange }) {
2  // ...
3  const mutation = useMutation(addUser, {
4    onMutate: () => {
5      onStatusChange('pending')
6    },
7    onSuccess: () => {
8      onStatusChange('success')
9      // ...
10    },
11    onError: () => {
12      onStatusChange('error')
13      // ...
14    },
15  })
16  // ...
17}

然后,在父组件中接收 onStatusChange 回调并处理状态变化:

1function App() {
2  const [status, setStatus] = useState('')
3
4  const handleStatusChange = (newStatus) => {
5    setStatus(newStatus)
6  }
7
8  return (
9    <div>
10      <CreateUser onStatusChange={handleStatusChange} />
11      {status === 'pending' && <p>正在添加用户...</p>}
12      {status === 'success' && <p>用户添加成功!</p>}
13      {status === 'error' && <p>用户添加失败,请重试。</p>}
14    </div>
15  )
16}

现在,当添加用户的 mutation 进行中,父组件会接收到实时状态并显示相应的提示信息。

useMutation 是 react-query 提供的一个自定义 hook,用于执行数据修改操作(create/update/delete),并处理相关的状态变化。下面列举出 useMutation 提供的所有方法:

  1. mutate(data, options?): 用于手动触发 mutation 函数执行,并更新相关的状态变量。

● data: 要传递给 mutation 函数的数据。 ● options: 可选对象,包含以下属性: ○ onMutate: 在 mutation 函数执行之前调用的回调函数。 ○ onSuccess: 在 mutation 函数成功执行后调用的回调函数。 ○ onError: 在 mutation 函数执行出错后调用的回调函数。 ○ onSettled: 在 mutation 函数执行结束后调用的回调函数。 ○ variables: 要覆盖默认 mutationKey 的参数对象。 ○ update: 更新缓存的函数,用于手动更新缓存中的数据。 ○ optimisticUpdate: 在 mutation 函数执行期间进行乐观更新的函数。 ○ throwOnError: 是否在 mutation 函数执行出错时抛出异常。 ○ retry: 是否自动重试 mutation 函数,或者提供一个重试配置对象。

  1. mutateAsync(data, options?): 与 mutate 方法类似,但使用 async/await 语法来执行 mutation 函数,并返回一个 Promise 对象。不会触发 isLoading 和 isError 等状态变量的更新,需要手动处理结果。
  2. 返回值:一个包含以下属性的对象:

● mutate: 用于触发 mutation 函数执行的方法。 ● mutateAsync: 使用 async/await 语法来执行 mutation 函数,并返回一个 Promise 对象。不会触发 isLoading 和 isError 等状态变量的更新,需要手动处理结果。 ● reset: 重置 mutation 函数的状态,清除缓存中的数据并重新加载数据。 ● isLoading: 表示 mutation 函数是否在执行的布尔值。 ● isError: 表示 mutation 函数执行期间是否出错的布尔值。 ● error: 如果 mutation 函数执行出错,则为错误对象。 ● data: 如果 mutation 函数执行成功,则为返回的数据。