React查询中分页的详细指南

602 阅读2分钟

这是使用React Query与TypeScript的系列文章中的第三篇。

以前的帖子:

这篇文章将介绍如何用React Query来翻阅星球大战的人物集合,提供流畅的用户体验:

Paging

查询客户端提供者

React Query需要在使用它的组件上面有一个QueryClientProvider 组件:

import { QueryClient, QueryClientProvider } from "react-query";
...

const queryClient = new QueryClient();
render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  rootElement
);

我们在组件树的顶部添加了QueryClientProvider ,以使React Query对任何组件可用。

抓取功能

我们的获取函数如下:

async function getData(params: { queryKey: [string, { page: number }] }) {
  const [, { page }] = params.queryKey;
  const response = await fetch(`https://swapi.dev/api/people/?page=${page}`);
  if (!response.ok) {
    throw new Error("Problem fetching data");
  }
  const data = await response.json();
  assertIsCharacterResponse(data);
  return data;
}

查询键被传入该函数,它是一个元组,第二元素包含要请求的数据的页码。

该函数发出一个简单的请求,如果不成功就会引发一个错误。

数据的类型用assertIsCharacterResponse type assert函数断定为CharacterResponse

type CharacterResponse = {
  results: Character[];
  next: string;
  previous: string;
};
type Character = {
  name: string;
};
function assertIsCharacterResponse(
  response: any
): asserts response is CharacterResponse {
  if (
    !("results" in response && "next" in response && "previous" in response)
  ) {
    throw new Error("Not results");
  }
  if (response.results.length > 0) {
    const firstResult = response.results[0];
    if (!("name" in firstResult)) {
      throw new Error("Not characters");
    }
  }
}

查询

useQuery 钩子可以在React组件中使用,如下所示,以调用获取的函数:

import { useQuery } from "react-query";
...
export function App() {
  const [page, setPage] = React.useState(1);

  const { status, error, data } = useQuery<
    CharacterResponse,
    Error
  >(["characters", { page }], getData);

  ...
}

页码被存储在状态中,并被初始化为第一页。然后,页码被传递到获取函数中。

我们把通常的状态变量从useQuery ,以便在获取过程的不同部分渲染不同的元素。

渲染列表

组件内部的渲染过程如下:

export function App() {
  ...

  if (status === "loading") {
    return <div>...</div>;
  }
  if (status === "error") {
    return <div>{error!.message}</div>;
  }
  if (data === undefined) {
    return null;
  }
  return (
    <div>
      <div>
        <button
          disabled={page <= 1}
          onClick={() => setPage((p) => p - 1)}
        >
          Previous
        </button>
        <span>Page: {page}</span>
        <button
          disabled={!data.next}
          onClick={() => setPage((p) => p + 1)}
        >
          Next
        </button>
      </div>
      {
        <div>
          {data.results.map((d) => (
            <div key={d.name}>{d.name}</div>
          ))}
        </div>
      }
    </div>
  );
}

当数据被加载时,上一页和下一页的分页按钮与数据页一起被渲染出来。

这个功能是正确的,但分页有点不正常:

Janky paging

keepPreviousData

useQuery 钩子里有一个叫做keepPreviousData 的选项,它允许在新的请求的数据取代它之前保留之前的数据。我们可以添加这个选项,如下所示:

const { status, error, data } = useQuery<
  CharacterResponse,
  Error
>(["characters", { page }], getData, { \
  keepPreviousData: true });

还有一个isPreviousData 的状态变量,可以被解构:

const {
  status,
  error,
  data,
  isPreviousData,} = useQuery<CharacterResponse, Error>(
  ["characters", { page }],
  getData,
  { keepPreviousData: true }
);

这告诉我们渲染时是否会使用以前的数据。

我们现在可以对寻呼机按钮做如下调整:

<div>
  <button
    disabled={isPreviousData || page <= 1}    onClick={() => setPage((p) => p - 1)}
  >
    Previous
  </button>
  <span>Page: {page}</span>
  <button
    disabled={isPreviousData || !data.next}    onClick={() => setPage((old) => old + 1)}
  >
    Next
  </button>
</div>

当数据被获取时,这将禁用这些按钮。

现在我们可以看到,分页变得更加顺畅了:

Smooth paging

很好 😊

这篇文章中的代码可以在CodeSandbox中找到:https://codesandbox.io/s/react-query-paging-trxxk?file=/src/App.tsx