0%

TanStack生态系统——React Query, Router, Table深度实践

TanStack(原React Training)生态系统为现代React应用提供了完整的解决方案,涵盖数据获取、路由管理、表格处理等关键领域。

介绍

  TanStack生态系统是一系列高质量开源工具的集合,最初由Kent C. Dodds和Tanner Linsley等人开发,包括React Query、React Router、TanStack Table等知名库。这些工具解决了React应用开发中的核心问题,成为现代前端开发的重要组成部分。本文将深入探讨TanStack生态系统的核心组件及其最佳实践。

React Query (TanStack Query)

简介

React Query是客户端状态管理库,专门用于处理服务器状态(Server State),包括数据获取、缓存、同步和更新等。

  • 核心功能
    • 自动缓存和数据去重
    • 请求取消和防抖
    • 后台数据预加载
    • 数据失效和重新验证
    • 乐观更新
    • 错误重试机制

安装与基础配置

1
npm install @tanstack/react-query
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// App.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟内认为数据新鲜
cacheTime: 1000 * 60 * 10, // 10分钟内保持缓存
retry: 3, // 默认重试3次
refetchOnWindowFocus: false, // 窗口聚焦时不自动刷新
},
},
});

function App() {
return (
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<UsersPage />} />
</Routes>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}

数据获取实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// hooks/useUsers.js
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';

const USERS_QUERY_KEY = ['users'];

// 获取用户列表
export const useUsers = (page = 1, limit = 10) => {
return useQuery({
queryKey: [...USERS_QUERY_KEY, page, limit],
queryFn: async () => {
const response = await fetch(`/api/users?page=${page}&limit=${limit}`);
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
},
staleTime: 1000 * 60, // 1分钟后变为stale
gcTime: 1000 * 60 * 5, // 5分钟后垃圾回收
placeholderData: (previousData) => previousData, // 使用上一次的数据作为占位符
});
};

// 获取单个用户
export const useUser = (userId) => {
return useQuery({
queryKey: [...USERS_QUERY_KEY, userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user ${userId}`);
}
return response.json();
},
enabled: !!userId, // 仅当userId存在时才启用查询
});
};

// 用户操作mutation
export const useCreateUser = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
},
onSuccess: (newUser) => {
// 乐观更新
queryClient.setQueryData(USERS_QUERY_KEY, (old) => {
if (!old) return { users: [newUser], total: 1 };
return {
...old,
users: [newUser, ...old.users],
total: old.total + 1,
};
});
},
onError: (error) => {
console.error('Create user error:', error);
},
});
};

高级功能应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// components/UserManagement.jsx
import React from 'react';
import { useUsers, useCreateUser } from '../hooks/useUsers';
import { useQueryClient } from '@tanstack/react-query';

const UserManagement = () => {
const queryClient = useQueryClient();
const [searchTerm, setSearchTerm] = React.useState('');

// 带搜索参数的查询
const { data, isLoading, isError, error, isPreviousData } = useUsers(1, 20);
const createUserMutation = useCreateUser();

// 搜索防抖
React.useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchTerm) {
// 手动触发查询
queryClient.invalidateQueries({
queryKey: ['users'],
predicate: (query) => query.queryKey.includes(searchTerm.toLowerCase()),
});
}
}, 500);

return () => clearTimeout(timeoutId);
}, [searchTerm, queryClient]);

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;

const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const userData = Object.fromEntries(formData);

try {
await createUserMutation.mutateAsync(userData);
e.target.reset();
} catch (error) {
alert(error.message);
}
};

return (
<div className="user-management">
<form onSubmit={handleSubmit} className="create-user-form">
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" required type="email" />
<button type="submit" disabled={createUserMutation.isPending}>
{createUserMutation.isPending ? 'Creating...' : 'Create User'}
</button>
</form>

<div className="users-list">
{data?.users?.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>

{isPreviousData && <div>Displaying cached data...</div>}
</div>
);
};

TanStack Router

简介

TanStack Router是全新的路由库,提供类型安全、嵌套路由、加载状态管理等功能,是React Router的现代替代方案。

  • 核心特性
    • 完全类型安全的路由定义
    • 嵌套路由和布局
    • 声明式路由配置
    • 加载状态管理
    • 代码分割支持

安装与配置

1
npm install @tanstack/react-router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// router/router.jsx
import { Router } from '@tanstack/react-router';
import { routeTree } from './routeTree';
import { NotFound } from '../components/NotFound';

// 创建路由器实例
export const router = new Router({
routeTree,
defaultNotFoundComponent: NotFound,
context: {
// 可以在这里注入全局依赖
queryClient: new QueryClient(),
},
});

// 在main.jsx中使用
import { RouterProvider } from '@tanstack/react-router';
import { router } from './router/router';

const rootElement = document.getElementById('root');
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// router/routeTree.jsx
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
import { Outlet, Link } from '@tanstack/react-router';

// 根路由组件
const RootComponent = () => {
return (
<>
<div className="nav">
<Link to="/">Home</Link>
<Link to="/users">Users</Link>
<Link to="/about">About</Link>
</div>
<hr />
<Outlet />
</>
);
};

// 根路由定义
const rootRoute = createRootRoute({
component: RootComponent,
});

// 首页路由
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: IndexComponent,
});

// 用户页面路由
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
component: UsersComponent,
loader: async ({ context }) => {
// 预加载数据
return await context.queryClient.ensureQueryData({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
},
});

// 用户详情路由
const userDetailRoute = createRoute({
getParentRoute: () => usersRoute,
path: '$userId',
component: UserDetailComponent,
loader: async ({ params: { userId }, context }) => {
return await context.queryClient.ensureQueryData({
queryKey: ['users', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
},
});

// 路由树
export const routeTree = rootRoute.addChildren([
indexRoute,
usersRoute.addChildren([userDetailRoute]),
]);

路由参数和查询参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// routes/UserDetailComponent.jsx
import { useParams, useSearch, useRouter } from '@tanstack/react-router';
import { useUser } from '../hooks/useUser';

const UserDetailComponent = () => {
const { userId } = useParams({ from: '/users/$userId' });
const { tab = 'overview' } = useSearch({ from: '/users/$userId' });
const router = useRouter();

const { data: user, isLoading, error } = useUser(userId);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

const handleTabChange = (newTab) => {
router.navigate({
search: (prev) => ({ ...prev, tab: newTab }),
});
};

return (
<div className="user-detail">
<h1>{user?.name}</h1>
<nav>
<button
className={tab === 'overview' ? 'active' : ''}
onClick={() => handleTabChange('overview')}
>
Overview
</button>
<button
className={tab === 'settings' ? 'active' : ''}
onClick={() => handleTabChange('settings')}
>
Settings
</button>
</nav>

<div className="tab-content">
{tab === 'overview' && <UserOverview user={user} />}
{tab === 'settings' && <UserSettings user={user} />}
</div>
</div>
);
};

TanStack Table

简介

TanStack Table(原React Table)是轻量级、可扩展的表格库,专注于提供核心表格功能而不限制UI实现。

  • 核心特点
    • 头等函数支持(First-class function)
    • UI框架无关
    • 完全可控和非可控模式
    • 丰富的插件系统
    • 高性能虚拟滚动

安装与基本使用

1
npm install @tanstack/react-table
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// components/UserTable.jsx
import React from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
} from '@tanstack/react-table';

const UserTable = ({ data, columns }) => {
const [sorting, setSorting] = React.useState([]);
const [columnFilters, setColumnFilters] = React.useState([]);
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
});

const table = useReactTable({
data,
columns,
state: {
sorting,
columnFilters,
pagination,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
debugTable: true, // 开发时启用调试
});

return (
<div className="p-2">
<div className="h-2" />
<table className="border-collapse border border-gray-300 rounded-lg overflow-hidden">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
className="border border-gray-300 bg-gray-100 p-2"
colSpan={header.colSpan}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<button
onClick={header.column.getToggleSortingHandler()}
className="ml-2"
>
{header.column.getNextSortingOrder() === 'asc' ? '↑' :
header.column.getNextSortingOrder() === 'desc' ? '↓' : '↕'}
</button>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="border border-gray-300 p-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>

<div className="h-2" />
<div className="flex items-center gap-2">
<button
className="border rounded p-1"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
{'<<'}
</button>
<button
className="border rounded p-1"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{'<'}
</button>
<button
className="border rounded p-1"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{'>'}
</button>
<button
className="border rounded p-1"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{'>>'}
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</strong>
</span>
<select
value={table.getState().pagination.pageSize}
onChange={e => {
table.setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
);
};

高级表格功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// utils/columnHelpers.js
import { createColumnHelper } from '@tanstack/react-table';

const columnHelper = createColumnHelper();

// 定义复杂表格列
export const userColumns = [
columnHelper.accessor('id', {
header: 'ID',
cell: info => info.getValue(),
footer: info => info.column.id,
}),
columnHelper.accessor(row => row.name, {
id: 'name',
cell: info => <input value={info.getValue()} onChange={e => console.log(e.target.value)} />,
header: () => <span>Name</span>,
footer: info => info.column.id,
}),
columnHelper.accessor('email', {
header: () => 'Email',
cell: info => (
<a href={`mailto:${info.getValue()}`} className="text-blue-500 hover:underline">
{info.getValue()}
</a>
),
}),
columnHelper.group({
header: 'Info',
footer: props => props.column.id,
columns: [
columnHelper.accessor('role', {
header: 'Role',
footer: props => props.column.id,
}),
columnHelper.accessor('isActive', {
header: 'Active',
cell: info => (
<span className={info.getValue() ? 'text-green-500' : 'text-red-500'}>
{info.getValue() ? 'Yes' : 'No'}
</span>
),
}),
],
}),
columnHelper.display({
id: 'actions',
cell: (props) => (
<div className="flex gap-2">
<button
className="text-blue-500 hover:text-blue-700"
onClick={() => editUser(props.row.original)}
>
Edit
</button>
<button
className="text-red-500 hover:text-red-700"
onClick={() => deleteUser(props.row.original.id)}
>
Delete
</button>
</div>
),
}),
];

// 自定义排序和过滤
export const createCustomColumn = (accessor, header, options = {}) => {
return columnHelper.accessor(accessor, {
header,
sortingFn: options.sortingFn || 'alphanumeric',
filterFn: options.filterFn || 'includesString',
enableSorting: options.enableSorting !== false,
enableColumnFilter: options.enableColumnFilter !== false,
...options,
});
};

集成实践案例

综合应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// pages/UsersDashboard.jsx
import React from 'react';
import { useUsers } from '../hooks/useUsers';
import { UserTable } from '../components/UserTable';
import { userColumns } from '../utils/columnHelpers';

const UsersDashboard = () => {
const { data, isLoading, isError, error } = useUsers();
const [globalFilter, setGlobalFilter] = React.useState('');

if (isLoading) return <div className="loading">Loading users...</div>;
if (isError) return <div className="error">Error: {error.message}</div>;

const filteredData = data?.users?.filter(user =>
Object.values(user).some(val =>
String(val).toLowerCase().includes(globalFilter.toLowerCase())
)
) || [];

return (
<div className="users-dashboard">
<h1>Users Management</h1>

<div className="controls mb-4">
<input
type="text"
placeholder="Search all columns..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="border p-2 rounded"
/>
</div>

<UserTable
data={filteredData}
columns={userColumns}
/>

<div className="stats mt-4 p-4 bg-gray-50 rounded">
<p>Total users: {data?.total || 0}</p>
<p>Active users: {
data?.users?.filter(user => user.isActive).length || 0
}</p>
</div>
</div>
);
};

export default UsersDashboard;

性能优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// hooks/useOptimizedTable.jsx
import { useMemo } from 'react';
import { useReactTable } from '@tanstack/react-table';

export const useOptimizedTable = (data, columns, options = {}) => {
// 使用useMemo优化数据和列定义
const memoizedData = useMemo(() => data, [data]);
const memoizedColumns = useMemo(() => columns, [columns]);

return useReactTable({
data: memoizedData,
columns: memoizedColumns,
...options,
// 虚拟化配置
enableVirtualization: true,
virtualizationOptions: {
overscan: 5,
},
});
};

// 组件级别的优化
const OptimizedTable = React.memo(({ data, columns }) => {
const table = useOptimizedTable(data, columns);

return (
<table>
{/* 表格渲染逻辑 */}
</table>
);
});

最佳实践与注意事项

1. React Query最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误处理策略
const useRobustQuery = (key, fetcher) => {
return useQuery({
queryKey: key,
queryFn: fetcher,
retry: (failureCount, error) => {
// 根据错误类型决定是否重试
if (error.status === 404) return false; // 404错误不重试
if (failureCount >= 3) return false; // 最多重试3次
return true;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
});
};

// 查询预取
export const prefetchUserData = async (queryClient, userId) => {
await queryClient.prefetchQuery({
queryKey: ['users', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
};

2. TanStack Router最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 类型安全的路由定义
import { createFileRoute } from '@tanstack/react-router';

type UserParams = { userId: string };
type UserSearch = { tab?: string; filter?: string };

export const Route = createFileRoute('/users/$userId')({
validateSearch: (search): UserSearch => {
return {
tab: search.tab ?? 'overview',
filter: search.filter,
};
},
loader: async ({ params, context }) => {
const user = await context.queryClient.ensureQueryData({
queryKey: ['users', params.userId],
queryFn: () => fetch(`/api/users/${params.userId}`).then(r => r.json()),
});

if (!user) {
throw new Error('User not found');
}

return { user };
},
component: UserDetailComponent,
});

3. TanStack Table最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 可重用的表格组件工厂
export const createDataTable = (columns, options = {}) => {
return React.forwardRef(({ data, ...props }, ref) => {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
...options,
});

return (
<div className="data-table" ref={ref}>
<table>
{/* 渲染表格内容 */}
</table>
</div>
);
});
};

// 表格状态管理
export const useTableState = (initialState = {}) => {
const [sorting, setSorting] = React.useState(initialState.sorting || []);
const [filters, setFilters] = React.useState(initialState.filters || []);
const [pagination, setPagination] = React.useState(initialState.pagination || {
pageIndex: 0,
pageSize: 10,
});

return {
sorting,
setSorting,
filters,
setFilters,
pagination,
setPagination,
};
};

生态集成

与其他库的集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 与Formik表单集成
import { useFormik } from 'formik';
import { useCreateUser } from '../hooks/useUsers';

const CreateUserForm = () => {
const createUser = useCreateUser();

const formik = useFormik({
initialValues: {
name: '',
email: '',
role: 'user',
},
onSubmit: async (values) => {
await createUser.mutateAsync(values);
},
});

return (
<form onSubmit={formik.handleSubmit}>
<input
name="name"
value={formik.values.name}
onChange={formik.handleChange}
/>
<input
name="email"
value={formik.values.email}
onChange={formik.handleChange}
/>
<button type="submit" disabled={createUser.isPending}>
{createUser.isPending ? 'Creating...' : 'Create'}
</button>
</form>
);
};

// 与Styled Components集成
import styled from 'styled-components';

const StyledTable = styled.table`
width: 100%;
border-collapse: collapse;

th, td {
padding: 12px;
border-bottom: 1px solid #ddd;
text-align: left;
}

tr:hover {
background-color: #f5f5f5;
}
`;

TanStack生态系统提供了完整的React应用解决方案,合理组合使用这些工具可以大大提高开发效率和用户体验。选择合适的组件组合需要根据项目需求和团队技术栈来决定。

总结

  TanStack生态系统为现代React应用开发提供了强大而灵活的工具集。React Query解决了服务端状态管理的复杂性,TanStack Router提供了类型安全的路由解决方案,而TanStack Table则带来了高度可定制的表格功能。通过深入了解和实践这些工具,开发者可以构建出高效、可靠且用户体验优秀的现代Web应用。

  在未来的发展中,TanStack团队将继续优化这些工具的性能,增强类型安全,并可能引入更多创新功能。开发者应该持续关注这些工具的更新,及时采用新的最佳实践来提升项目质量。

bulb