React TanStack

使用React Tanstack通过API请求读写表格数据

了解如何使用React Tanstack通过API请求读取、创建、更新和删除表格数据。

2024年2月15日
React-Table-API
分享

在本系列的前面部分中,我们学习了如何创建可编辑的动态表格、动态添加和删除行以及验证输入字段和行。到目前为止,我们一直在处理对象数组中的表格数据。在本文中,我们将学习如何使用JSON ServerSWR通过 API 请求读取和写入表格数据。

  • JSON Server 是一个假的 REST API,我们可以用它来测试我们的 API 请求。
  • SWR 是一个用于数据获取的 React hook,可以轻松获取和重新验证数据。

设置并安装依赖项

在开始之前,让我们安装本文所需的依赖项。

# Install JSON Server
npm install -g json-server

# Install SWR
npm install swr

为了让 JSON Server 工作,我们需要创建一个db.json文件。该文件将包含我们想要获取和更新的数据。在我们的例子中,它将是学生的列表。

让我们在src/Table文件夹中创建一个新的db.json文件并添加以下数据

{
  "students": [
    {
      "id": 1,
      "studentNumber": 1111,
      "name": "Bahar Constantia",
      "dateOfBirth": "1984-01-04",
      "major": "Computer Science"
    },
    {
      "id": 2,
      "studentNumber": 2222,
      "name": "Harold Nona",
      "dateOfBirth": "1961-05-10",
      "major": "Communications"
    },
    {
      "id": 3,
      "studentNumber": 3333,
      "name": "Raginolf Arnulf",
      "dateOfBirth": "1991-10-12",
      "major": "Business"
    },
    {
      "id": 4,
      "studentNumber": 4444,
      "name": "Marvyn Wendi",
      "dateOfBirth": "1978-09-24",
      "major": "Psychology"
    }
  ]
}

请注意,我们向每个学生对象添加了一个新的id字段。该字段将作为 JSON Server 更新和删除数据的唯一标识符。我们还将studentId字段更改为studentNumber,以将其与id字段区分开来。

要启动并运行 JSON Server,我们需要在终端中运行以下命令。

json-server --watch ./src/Table/db.json --port 5000

这将在端口 5000 上运行服务器并提供db.json文件中的数据。我们可以通过访问http://localhost:5000/students来访问数据。

tanstack4-1

在两个终端中分别运行两个服务器(React 和 JSON Server)的另一种方法是使用Concurrently。它是一个允许我们同时运行多个命令的包。

首先,让我们安装该软件包。

npm install concurrently

然后,我们需要向package.json文件添加一个新脚本,以使我们能够同时运行两个服务器。

"scripts": {
  "vite": "vite",
  "jsonserver": "json-server ./src/Table/db.json --watch --port 5000",
  "dev": "concurrently \"npm run jsonserver\" \"npm run vite\"",
  ...
}

现在,当使用npm run dev正常运行项目时,两台服务器将同时运行。

使用 SWR 获取数据

现在我们已经运行了 JSON Server,让我们开始处理 API 请求。

在开始之前,我们需要进行一些更改以适应新的数据结构。

1 必须更新中的类型Student接口data.ts以匹配文件中的数据db.json

export type Student = {
  id: number
  studentNumber: number
  name: string
  dateOfBirth: string
  major: string
}

2 由于数据现在存储在 JSON 文件中,因此我们不再需要data.ts中的data对象数组。我们只需要Student类型接口。为了使代码更清晰,让我们将Student类型移动到“Table”文件夹中的新的types.ts文件中。所以现在Table组件中的/data引用将更改为/types.

import { Student } from './types';

3 columns.ts中的columns数组需要更新以匹配新的Student类型接口

...
import { Student } from './types';
...
export const columns = [
  columnHelper.accessor('studentNumber', {
    header: 'Student Id',
    cell: TableCell,
    meta: {
      type: 'number',
    },
  }),
  ...
]

我们已经准备好接受这些变化。让我们开始获取数据。

首先,我们将创建一个新的useStudents钩子。该钩子将负责使用useSWR获取数据和其他与学生相关的操作。以下是一些需要注意的事项:

  1. 使用useSWR钩子时,它接受两个参数。第一个参数是将用于获取数据的 URL。在我们的例子中,它是http://localhost:5000/students。第二个参数是getRequest,用于获取数据的函数。
  2. useStudents钩子会将学生的data数组、isValidating变量公开给使用它的组件。isValidating将用于确定是否正在获取数据。它与isLoading类似,但也会在修改时触发。
import useSWR from 'swr';

const url = 'http://localhost:5000/students';

async function getRequest() {
  const response = await fetch(url);
  return response.json();
}

export default function useStudents() {
  const { data, isValidating } = useSWR(url, getRequest);

  return {
    data: data ?? [],
    isValidating,
  };
}

然后,我们将使用Table组件中的useStudents钩子来获取数据,替换前面部分中的originalData数组。

快速回顾一下,originalData数组是包含原始数据的数组,用于初始化data数组。我们必须这样做的原因是因为我们需要保留以前的数据,以防用户取消更改。现在,由于originalData数据来自服务器,我们可以删除之前的数据。

import { useEffect, useState } from "react";
...
import useStudents from "./useStudents";

export const Table = () => {
  const { data: originalData, isValidating } = useStudents();
  const [data, setData] = useState<Student[]>([]);
  const [editedRows, setEditedRows] = useState({});
  const [validRows, setValidRows] = useState({});

  useEffect(() => {
    if (isValidating) return;
    setData([...originalData]);
  }, [isValidating]);

  ...
};

由于我们不知道数据何时准备好,因此我们必须添加一个useEffect钩子来在originalData更改时更新data数组。这时候isValidating变量就派上用场了。这将在获取数据时是true,并在准备就绪时是false

当然,我们总是可以添加加载指示器来改善用户体验,但这超出了本教程的范围。

现在,如果我们运行该项目并打开浏览器网络选项卡,我们将看到正在从服务器获取数据。

tanstack4-2
注意:此时代码的其他部分可能会中断,这是预期的,并将在后续步骤中修复。如果错误阻止我们运行项目,我们随时可以注释掉这些错误。

更新数据

更新表格的行需要向服务器发出新的PUT请求,并执行几个附加步骤:

  1. 更新函数需要两个参数。第一个参数是要更新的行的id。第二个参数是需要更新的行的数据。
  2. 我们需要从useStudents钩子中公开更新函数,以便Table组件可以使用它。
  3. 更新数据后,我们需要触发一个mutate函数来更新originalData数组。这个mutate是 SWR 中的内置函数,允许我们同步和重新验证数据。
import useSWR, { mutate } from 'swr';
import { Student } from './types';

const url = 'http://localhost:5000/students';

async function updateRequest(id: number, data: Student) {
  const response = await fetch(`${url}/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  return response.json();
}

async function getRequest() {
  const response = await fetch(url);
  return response.json();
}

export default function useStudents() {
  const { data, isValidating } = useSWR(url, getRequest);

  const updateRow = async (id: number, postData: Student) => {
    await updateRequest(id, postData);
    mutate(url);
  };

  return {
    data: data ?? [],
    isValidating,
    updateRow
  };
}

Table组件中,我们有两个更新数据的函数。第一个是updateData,在单元格输入更改时触发的,它更新指定行索引处的data数组。第二个是revertData,当用户单击取消/保存按钮时触发,它要么复原更改,要么保留更改并更新originalData。这是我们从前面部分拿来的代码。

const table = useReactTable({
  ...
  revertData: (rowIndex: number, revert: boolean) => {
        if (revert) {
          setData((old) =>
            old.map((row, index) =>
              index === rowIndex ? originalData[rowIndex] : row
            )
          );
        } else {
          setOriginalData((old) =>
            old.map((row, index) => (index === rowIndex ? data[rowIndex] : row))
          );
        }
      },
    ...
  }
)

现在,由于我们需要更新服务器上的数据,因此我们将更改上面的代码并创建一个名为updateRow的单独函数,该函数将从useStudents钩子触发该updateRow函数。

...
export const Table = () => {
  const { data: originalData, isValidating, updateRow } = useStudents();
  ...
  const table = useReactTable({
    ...
    revertData: (rowIndex: number) => {
      setData((old) =>
        old.map((row, index) =>
          index === rowIndex ? originalData[rowIndex] : row
        )
      );
    },
    updateRow: (rowIndex: number) => {
      updateRow(data[rowIndex].id, data[rowIndex]);
    },
    ...
  })
}

请注意,由于我们从服务器上更新数据,因此不再需要setOriginalData函数,而服务器将自动更新originalData数组。

最后,在EditCell组件中,我们需要更新setEditedRows来为保存按钮包含updateRow函数。

...
export const EditCell = ({ row, table }) => {
  ...
  const setEditedRows = (e: MouseEvent<HTMLButtonElement>) => {
    const elName = e.currentTarget.name;
    meta?.setEditedRows((old: []) => ({
      ...old,
      [row.id]: !old[row.id],
    }));
    if (elName !== "edit") {
      e.currentTarget.name === "cancel" ? meta?.revertData(row.index) : meta?.updateRow(row.index);
    }
  };
  ...
};

下面是更新过程,其中网络选项卡打开以演示 API 请求。

tanstack4-3

创建数据

添加新行与更新方法类似,只是 API 请求是一个POST方法。

让我们创建两个函数,一个用于 API 请求,另一个用于Table组件中将使用的钩子。

import useSWR, { mutate } from 'swr';
import { Student } from './types';

const url = 'http://localhost:5000/students';

async function updateRequest(id: number, data: Student) {
  const response = await fetch(`${url}/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  return response.json();
}

async function addRequest(data: Student) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  return response.json();
}

async function getRequest() {
  const response = await fetch(url);
  return response.json();
}

export default function useStudents() {
  const { data, isValidating } = useSWR(url, getRequest);

  const updateRow = async (id: number, postData: Student) => {
    await updateRequest(id, postData);
    mutate(url);
  };

  const addRow = async (postData: Student) => {
    await addRequest(postData);
    mutate(url);
  };

  return {
    data: data ?? [],
    isValidating,
    addRow,
    updateRow
  };
}

以前,在Table组件中,我们通常使用新添加的行来更新dataoriginalData

const table = useReactTable({
  ...
  addRow: () => {
  const newRow: Student = {
    studentId: Math.floor(Math.random() * 10000),
    name: "",
    dateOfBirth: "",
    major: "",
  };
  const setFunc = (old: Student[]) => [...old, newRow];
    setData(setFunc);
    setOriginalData(setFunc);
  },
  ...
})

现在,由于我们要将数据添加到服务器,因此我们只需要从useStudents钩子中触发addRow函数即可。我们不再需要任何set函数,因为在添加请求之后,mutate函数将更新originalData数组,因此useEffect钩子将更新data数组。

...
export const Table = () => {
  const { data: originalData, isValidating, addRow, updateRow } = useStudents();
  ...
  const table = useReactTable({
    ...
    addRow: () => {
      const id = Math.floor(Math.random() * 10000);
      const newRow: Student = {
        id,
        studentNumber: id,
        name: "",
        dateOfBirth: "",
        major: ""
      };
      addRow(newRow);
    },
    ...
  })
  ...
}

此外,我们还向newRow对象添加了一个id字段。下面是添加过程,其中网络选项卡打开以演示 API 请求。

tanstack4-4

删除数据

此时,我们有负责获取、更新和添加数据的useStudents钩子。我们需要做的最后一件事是添加删除功能。

让我们创建一个名为deleteRequest的新函数,它将负责 API 请求,然后将其添加到useStudents钩子中。

import useSWR, { mutate } from 'swr';
import { Student } from './types';

const url = 'http://localhost:5000/students';

async function updateRequest(id: number, data: Student) {
  const response = await fetch(`${url}/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  return response.json();
}

async function addRequest(data: Student) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  return response.json();
}

async function deleteRequest(id: number) {
  const response = await fetch(`${url}/${id}`, {
    method: 'DELETE',
    headers: {
      'Content-Type': 'application/json',
    }
  });
  return response.json();
}

async function getRequest() {
  const response = await fetch(url);
  return response.json();
}

export default function useStudents() {
  const { data, isValidating } = useSWR(url, getRequest);

  const updateRow = async (id: number, postData: Student) => {
    await updateRequest(id, postData);
    mutate(url);
  };

  const deleteRow = async (id: number) => {
    await deleteRequest(id);
    mutate(url);
  };

  const addRow = async (postData: Student) => {
    await addRequest(postData);
    mutate(url);
  };

  return {
    data: data ?? [],
    isValidating,
    addRow,
    updateRow,
    deleteRow
  };
}

然后,在Table组件中,与上一步类似,我们删除set函数并从useStudents钩子中触发deleteRow函数。

...
export const Table = () => {
  const { data: originalData, isValidating, addRow, updateRow, deleteRow } = useStudents();
  ...
  const table = useReactTable({
    ...
    removeRow: (rowIndex: number) => {
      deleteRow(data[rowIndex].id);
    },
    ...
  })
  ...
}

最后,由于我们在上一部分中启用了删除多行,因此我们需要更新removeSelectedRows函数以循环每一行并触发deleteRow函数。

...
export const Table = () => {
  const { data: originalData, isValidating, addRow, updateRow, deleteRow } = useStudents();
  ...
  const table = useReactTable({
    ...
    removeRow: (rowIndex: number) => {
      deleteRow(data[rowIndex].id);
    },
    removeSelectedRows: (selectedRows: number[]) => {
      selectedRows.forEach((rowIndex) => {
        deleteRow(data[rowIndex].id);
      });
    },
    ...
  })
  ...
}

下面是删除过程,其中网络选项卡打开以演示 API 请求。

tanstack4-5

完整代码

完整的代码可以在这个仓库中找到。如果您喜欢本教程,请为仓库加星,并随时提出新功能的需求!

总结

在这一部分中,我们学习了如何使用 React Tanstack 通过 API 请求读取和写入表格数据。我们使用 JSON Server 创建一个假的 REST API 和 SWR 来获取和重新验证数据。我们还学习了如何从服务器更新、添加和删除数据。

来自:https://muhimasri.com/blogs/react-table-api-requests/

更多文章

了解如何使用React Tanstack验证表格的输入字段、选择字段和实现自定义验证。

2024年2月15日 · React TanStack
React-Add-Remove-Table-Rows
使用React TanStack实现有多行选择的表格的添加和删除行功能的指南。
2024年2月15日 · React TanStack
React-Editable-Table
使用TanStack的动态列模式创建React可编辑表格单元格和行的综合指南。
2024年2月14日 · React TanStack
refine
用于构建内部工具、管理面板、仪表板和B2B应用程序的React框架,具有无与伦比的灵活性。
2024年2月4日 · React