在本系列的前面部分中,我们学习了如何创建可编辑的动态表格、动态添加和删除行以及验证输入字段和行。到目前为止,我们一直在处理对象数组中的表格数据。在本文中,我们将学习如何使用JSON Server和SWR通过 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来访问数据。
在两个终端中分别运行两个服务器(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
获取数据和其他与学生相关的操作。以下是一些需要注意的事项:
- 使用
useSWR
钩子时,它接受两个参数。第一个参数是将用于获取数据的 URL。在我们的例子中,它是http://localhost:5000/students
。第二个参数是getRequest
,用于获取数据的函数。 - 该
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
。
当然,我们总是可以添加加载指示器来改善用户体验,但这超出了本教程的范围。
现在,如果我们运行该项目并打开浏览器网络选项卡,我们将看到正在从服务器获取数据。
注意:此时代码的其他部分可能会中断,这是预期的,并将在后续步骤中修复。如果错误阻止我们运行项目,我们随时可以注释掉这些错误。
更新数据
更新表格的行需要向服务器发出新的PUT
请求,并执行几个附加步骤:
- 更新函数需要两个参数。第一个参数是要更新的行的
id
。第二个参数是需要更新的行的数据。 - 我们需要从
useStudents
钩子中公开更新函数,以便Table
组件可以使用它。 - 更新数据后,我们需要触发一个
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 请求。
创建数据
添加新行与更新方法类似,只是 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
组件中,我们通常使用新添加的行来更新data
和originalData
。
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 请求。
删除数据
此时,我们有负责获取、更新和添加数据的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 请求。
完整代码
完整的代码可以在这个仓库中找到。如果您喜欢本教程,请为仓库加星,并随时提出新功能的需求!
总结
在这一部分中,我们学习了如何使用 React Tanstack 通过 API 请求读取和写入表格数据。我们使用 JSON Server 创建一个假的 REST API 和 SWR 来获取和重新验证数据。我们还学习了如何从服务器更新、添加和删除数据。