React
React 是一个由 Facebook 开发的流行的 JavaScript 库,用于构建交互式用户界面。它采用了一种基于组件化的开发模式,使得开发人员可以将 UI 拆分为独立的、可复用的组件,并由这些组件构建复杂的用户界面。
React 的主要特点包括:
组件化开发:React 将 UI 拆分为独立的、可复用的组件,开发人员可以将这些组件组合在一起构建复杂的用户界面;
虚拟 DOM:React 采用虚拟 DOM 技术来优化 UI 更新性能,通过比较前后状态的差异来最小化 DOM 操作;
单向数据流:React 中的数据流是单向的,数据由父组件传递给子组件,子组件不能直接修改父组件的数据;
JSX:React 支持使用 JSX 语法,将组件的结构和样式与 JavaScript 代码结合在一起,使得代码更加简洁和易于理解。
React 生态系统非常丰富,包括许多与 React 相关的库和工具,如 Redux、React Router、Webpack 等,可帮助开发人员更好地使用 React 构建应用程序。
React 在 Web 开发、移动应用开发和桌面应用开发等领域得到了广泛应用,并且在社区中有着非常活跃的开发者和贡献者。如果您想要学习构建交互式用户界面的技术,React 是一个非常不错的选择。

查看更多相关内容
如何创建和使用 Zustand store?### 创建 Zustand store 的步骤:
1. **安装 Zustand**:
```bash
npm install zustand
# 或
yarn add zustand
```
2. **创建 store 文件**(例如 `store.js`):
```javascript
import { create } from 'zustand';
const useStore = create((set) => ({
// 状态
count: 0,
user: null,
// 操作状态的方法
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null })
}));
export default useStore;
```
3. **在组件中使用 store**:
```javascript
import React from 'react';
import useStore from './store';
function Counter() {
// 方法 1:获取整个 store
const { count, increment, decrement } = useStore();
// 方法 2:选择性订阅(推荐,性能更好)
const countValue = useStore((state) => state.count);
const incrementCount = useStore((state) => state.increment);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
```
### 关键点:
* 使用 `create` 函数创建 store
* store 是一个函数,接收 `set` 和 `get` 两个参数
* `set` 用于更新状态,支持函数式更新
* 使用 `useStore` hook 在组件中访问状态
* 推荐使用选择性订阅来优化性能
服务端 · 3月7日 12:26
React Query 中的 useQuery 和 useMutation 钩子有什么区别,分别适用于什么场景?React Query 中的 `useQuery` 和 `useMutation` 是两个核心钩子,它们的区别和适用场景如下:
### useQuery
**功能**:用于执行只读操作,如获取数据。
**适用场景**:
- 获取列表数据(如用户列表、产品列表)
- 获取详情数据(如用户详情、订单详情)
- 任何需要从服务器读取数据而不修改的场景
**特点**:
- 自动缓存数据
- 支持数据失效和背景刷新
- 返回数据、加载状态、错误状态等
- 可以配置重试策略
**基本用法**:
```javascript
const { data, isLoading, error } = useQuery('todos', fetchTodos);
```
### useMutation
**功能**:用于执行修改操作,如创建、更新、删除数据。
**适用场景**:
- 提交表单数据
- 更新用户信息
- 删除资源
- 任何需要修改服务器数据的场景
**特点**:
- 支持乐观更新
- 可以配置成功/失败回调
- 支持请求取消
- 可以触发相关查询的重新获取
**基本用法**:
```javascript
const mutation = useMutation(addTodo, {
onSuccess: () => {
// 成功回调,如重新获取数据
queryClient.invalidateQueries('todos');
},
});
// 调用方式
mutation.mutate(newTodo);
```
### 核心区别
1. **操作类型**:`useQuery` 用于读取操作,`useMutation` 用于写入操作
2. **缓存行为**:`useQuery` 自动缓存数据,`useMutation` 不缓存结果
3. **调用方式**:`useQuery` 自动执行(可配置),`useMutation` 需要手动调用 `mutate` 方法
4. **返回值**:`useQuery` 返回数据和状态,`useMutation` 返回 mutation 函数和状态
正确使用这两个钩子可以有效地管理应用中的数据获取和修改操作,提高开发效率和用户体验。
服务端 · 3月7日 12:25
React Query 与传统状态管理库(如 Redux)的区别是什么,什么时候应该使用 React Query?React Query 和传统状态管理库(如 Redux)在设计理念和使用场景上有显著区别:
### 核心区别
1. **管理的状态类型**:
- **React Query**:专门管理服务器状态(从API获取的数据)
- **Redux**:管理全局客户端状态(如用户偏好、UI状态、应用配置等)
2. **状态管理方式**:
- **React Query**:声明式数据获取,自动处理缓存、失效、重试等
- **Redux**:需要手动编写action、reducer来管理状态更新
3. **缓存机制**:
- **React Query**:内置强大的缓存系统,自动处理数据的缓存和失效
- **Redux**:没有内置缓存机制,需要手动实现或使用额外库
4. **数据获取**:
- **React Query**:集成了数据获取逻辑,支持自动重试、轮询等
- **Redux**:需要手动集成axios/fetch等库,处理异步逻辑
5. **代码复杂度**:
- **React Query**:减少了样板代码,API简洁直观
- **Redux**:需要编写更多样板代码(action types、actions、reducers)
### 适用场景
#### 何时使用 React Query:
1. **需要频繁与API交互的应用**:React Query 专门优化了服务器状态管理
2. **需要缓存数据的场景**:内置缓存机制减少重复请求
3. **需要乐观更新的场景**:提升用户体验
4. **需要处理分页、无限滚动的场景**:内置支持
5. **需要自动重试和错误处理的场景**:简化错误处理逻辑
#### 何时使用传统状态管理库:
1. **需要管理复杂的客户端状态**:如多级嵌套的UI状态
2. **需要中间件支持**:如日志、路由、持久化等
3. **需要时间旅行调试**:Redux DevTools提供强大的调试能力
4. **需要严格的状态变更控制**:如金融应用
### 最佳实践
实际上,React Query 和传统状态管理库并不是互斥的,它们可以结合使用:
- 使用 React Query 管理所有服务器状态
- 使用 Redux/Zustand 等管理客户端状态
- 这样可以充分发挥各自的优势,构建更高效、可维护的应用
React Query 的出现并不是要完全取代传统状态管理库,而是为了解决服务器状态管理这一特定领域的问题,让开发人员能够更专注于业务逻辑和UI开发。
服务端 · 3月7日 12:25
如何在 React Query 中处理错误和重试,有哪些最佳实践?React Query 提供了强大的错误处理和重试机制,帮助开发者构建更健壮的应用:
### 错误处理
1. **基本错误处理**:
```javascript
const { data, error, isError } = useQuery('todos', fetchTodos);
if (isError) {
return <div>Error: {error.message}</div>;
}
```
2. **全局错误处理**:通过 QueryClient 配置
```javascript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
console.error('Query error:', error);
// 可以在这里添加全局错误通知
},
},
},
});
```
3. **Mutation 错误处理**:
```javascript
const mutation = useMutation(addTodo, {
onError: (error, variables, context) => {
console.error('Mutation error:', error);
// 错误处理逻辑
},
});
```
### 重试机制
1. **基本重试配置**:
```javascript
const { data } = useQuery('todos', fetchTodos, {
retry: 3, // 默认值为 3
});
```
2. **高级重试配置**:
```javascript
const { data } = useQuery('todos', fetchTodos, {
retry: (failureCount, error) => {
// 自定义重试逻辑
if (error.status === 404) return false; // 404 不重试
if (failureCount >= 3) return false; // 最多重试 3 次
return true;
},
retryDelay: (attemptIndex) => {
// 指数退避策略
return Math.min(1000 * 2 ** attemptIndex, 30000);
},
});
```
### 最佳实践
1. **错误边界**:使用 React 错误边界捕获查询错误
```javascript
<ErrorBoundary fallback={<ErrorComponent />}>
<QueryComponent />
</ErrorBoundary>
```
2. **错误状态 UI**:为不同类型的错误提供不同的 UI 反馈
```javascript
if (isError) {
if (error.status === 401) {
return <div>请先登录</div>;
} else if (error.status === 404) {
return <div>资源不存在</div>;
} else {
return <div>发生错误:{error.message}</div>;
}
}
```
3. **重试策略**:
- 对网络错误使用重试
- 对 5xx 服务器错误使用重试
- 对 4xx 客户端错误(如 401、404)不使用重试
4. **错误日志**:集成错误监控服务(如 Sentry)
```javascript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
Sentry.captureException(error);
},
},
},
});
```
5. **用户反馈**:
- 显示加载状态
- 显示错误信息
- 提供重试按钮
6. **乐观更新错误处理**:在 mutation 中正确处理回滚
```javascript
const mutation = useMutation(updateTodo, {
onMutate: () => { /* 乐观更新 */ },
onError: (error, variables, context) => {
// 回滚数据
queryClient.setQueryData('todos', context.previousTodos);
// 显示错误信息
showNotification('更新失败', 'error');
},
});
```
通过合理配置错误处理和重试机制,可以显著提高应用的可靠性和用户体验。
服务端 · 3月7日 12:25
React Query 有哪些高级特性,如依赖查询、并行查询和窗口聚焦重新获取?React Query 提供了许多高级特性,使数据管理更加灵活和强大:
### 1. 依赖查询(Dependent Queries)
依赖查询是指一个查询的执行依赖于另一个查询的结果。
**使用场景**:当你需要先获取一个资源的 ID,然后用这个 ID 获取详细信息时。
**实现方式**:
```javascript
const { data: user } = useQuery(['user', userId], fetchUser);
// 只有当 user 存在时才执行第二个查询
const { data: userPosts } = useQuery(
['posts', user?.id],
() => fetchPosts(user.id),
{ enabled: !!user }
);
```
### 2. 并行查询(Parallel Queries)
并行查询是指同时执行多个独立的查询。
**使用场景**:当你需要在一个组件中获取多个不相关的数据时。
**实现方式**:
```javascript
// 方式1:多个 useQuery 钩子
const { data: users } = useQuery('users', fetchUsers);
const { data: posts } = useQuery('posts', fetchPosts);
// 方式2:使用 useQueries 钩子(React Query v3+)
const results = useQueries([
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['posts'], queryFn: fetchPosts },
]);
```
### 3. 窗口聚焦重新获取(Window Focus Refetching)
当用户重新聚焦浏览器窗口时,React Query 可以自动重新获取数据。
**使用场景**:确保用户看到的是最新数据,特别是在多标签页应用中。
**配置方式**:
```javascript
// 全局配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true, // 默认值
},
},
});
// 单个查询配置
const { data } = useQuery('todos', fetchTodos, {
refetchOnWindowFocus: false, // 禁用
});
```
### 4. 分页和无限滚动(Pagination & Infinite Scroll)
React Query 提供了专门的钩子来处理分页和无限滚动。
**实现方式**:
```javascript
// 分页查询
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
['posts', pageSize],
({ pageParam = 1 }) => fetchPosts(pageParam, pageSize),
{
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
},
}
);
```
### 5. 轮询(Polling)
自动定期重新获取数据。
**使用场景**:需要显示实时数据的应用,如仪表盘、聊天应用等。
**配置方式**:
```javascript
const { data } = useQuery('todos', fetchTodos, {
refetchInterval: 5000, // 每5秒重新获取
refetchIntervalInBackground: true, // 后台也轮询
});
```
### 6. 预取(Prefetching)
提前获取可能需要的数据,提升用户体验。
**实现方式**:
```javascript
// 手动预取
queryClient.prefetchQuery('todos', fetchTodos);
// 视口预取(使用 react-query/prefetch)
usePrefetchQuery('todos', fetchTodos);
```
### 7. 持久化缓存(Persisted Queries)
将缓存数据持久化到 localStorage 或其他存储中。
**实现方式**:使用 `persistQueryClient` 插件
这些高级特性使得 React Query 能够应对各种复杂的数据获取场景,提供更灵活、更高效的数据管理方案。
服务端 · 3月7日 12:25
React Query 如何与 React Suspense 集成使用,有哪些注意事项?React Query 支持与 React Suspense 集成,使得数据获取可以像处理组件渲染一样自然:
### 基本集成方法
1. **启用 Suspense 模式**:
```javascript
const { data } = useQuery('todos', fetchTodos, {
suspense: true,
});
```
2. **使用 Suspense 组件包裹**:
```javascript
function TodoList() {
const { data: todos } = useQuery('todos', fetchTodos, {
suspense: true,
});
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
);
}
```
### 错误边界处理
当使用 Suspense 模式时,错误需要通过 Error Boundary 捕获:
```javascript
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <div>Error: {this.state.error.message}</div>;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
</ErrorBoundary>
);
}
```
### 注意事项
1. **兼容性**:
- Suspense 模式需要 React 16.6+ 版本
- 确保所有相关组件都支持 Suspense
2. **错误处理**:
- 必须使用 Error Boundary 捕获错误
- 不能再使用 useQuery 返回的 error 和 isError 属性
3. **缓存行为**:
- 即使数据在缓存中,首次渲染时仍会触发 Suspense
- 后续渲染会直接使用缓存数据,不会触发 Suspense
4. **性能考虑**:
- Suspense 模式可能会导致组件树的多次渲染
- 对于复杂应用,需要权衡用户体验和性能
5. **与其他功能的兼容性**:
- **窗口聚焦重新获取**:可能会导致意外的 Suspense 触发
- **轮询**:需要谨慎配置,避免频繁触发 Suspense
6. **服务器端渲染**:
- 在 SSR 环境中使用 Suspense 需要特殊处理
- 推荐使用 `dehydrate` 和 `hydrate` 方法
### 最佳实践
1. **渐进式采用**:
- 先在非关键组件中尝试 Suspense 模式
- 逐步扩展到整个应用
2. **合理使用 fallback**:
- 提供有意义的加载状态
- 避免使用过于简单的加载指示器
3. **结合预取**:
- 使用 `queryClient.prefetchQuery` 提前获取数据
- 减少 Suspense 的触发次数
4. **错误边界策略**:
- 在适当的层级放置 Error Boundary
- 提供具体的错误信息和恢复选项
通过合理集成 React Query 和 Suspense,可以创建更加流畅、响应迅速的用户界面,同时保持代码的简洁性和可读性。
服务端 · 3月7日 12:25
如何在 Zustand 中优化状态更新和性能?### Zustand 中的性能优化方法:
1. **选择性订阅**:
```javascript
// 不推荐:订阅整个 store,会导致组件在任何状态变化时都重渲染
const { count, user } = useStore();
// 推荐:只订阅需要的状态部分
const count = useStore((state) => state.count);
const user = useStore((state) => state.user);
```
2. **使用 shallow 比较**(对于复杂对象):
```javascript
import { create } from 'zustand';
import { shallow } from 'zustand/shallow';
// 订阅多个状态并使用 shallow 比较
const { count, user } = useStore(
(state) => ({ count: state.count, user: state.user }),
shallow // 只有当 count 或 user 真正变化时才重渲染
);
```
3. **状态拆分**:
```javascript
// 按功能拆分多个 store
// userStore.js
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}));
// counterStore.js
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
```
4. **使用 get 访问当前状态**(避免闭包陷阱):
```javascript
const useStore = create((set, get) => ({
count: 0,
// 推荐:使用 get 获取最新状态
increment: () => set((state) => ({ count: state.count + 1 })),
// 也可以使用 get
incrementAsync: async () => {
await someAsyncOperation();
set({ count: get().count + 1 });
}
}));
```
5. **批量更新**:
```javascript
// Zustand 会自动批量处理多个 set 调用
const updateMultiple = () => {
set({ count: 1 });
set({ user: { name: 'John' } });
// 只会触发一次重渲染
};
```
6. **避免在组件渲染时创建新函数**:
```javascript
// 不推荐:每次渲染都创建新函数
const incrementBy = (value) => useStore.getState().incrementBy(value);
// 推荐:在 store 中定义方法
// 在 store 中:
incrementBy: (value) => set((state) => ({ count: state.count + value }))
// 在组件中:
const incrementBy = useStore((state) => state.incrementBy);
```
### 关键点:
* 选择性订阅是 Zustand 性能优化的核心
* 使用 shallow 比较可以优化复杂对象的订阅
* 状态拆分可以减少不必要的重渲染
* 合理使用 get 可以避免闭包陷阱
* Zustand 会自动处理批量更新
服务端 · 3月7日 12:01
如何在 Zustand 中创建自定义中间件?在 Zustand 中创建自定义中间件非常灵活,可以用来实现各种功能,如日志记录、状态验证、性能监控等。
### 基本自定义中间件结构:
```javascript
const customMiddleware = (config) => (set, get, api) => {
// 在原始 store 之前执行的逻辑
const originalSet = set;
// 包装 set 函数
const wrappedSet = (partial, replace) => {
// 在状态更新前执行逻辑
console.log('State will update:', partial);
// 调用原始 set
const result = originalSet(partial, replace);
// 在状态更新后执行逻辑
console.log('State updated:', get());
return result;
};
// 创建 store
const store = config(wrappedSet, get, api);
// 返回增强后的 store
return store;
};
```
### 示例 1:日志中间件
```javascript
const loggerMiddleware = (config) => (set, get, api) => {
const originalSet = set;
const wrappedSet = (partial, replace) => {
const previousState = get();
const result = originalSet(partial, replace);
const nextState = get();
console.log('Previous state:', previousState);
console.log('Action:', partial);
console.log('Next state:', nextState);
return result;
};
return config(wrappedSet, get, api);
};
// 使用
const useStore = create(
loggerMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))
);
```
### 示例 2:状态验证中间件
```javascript
const validationMiddleware = (schema) => (config) => (set, get, api) => {
const originalSet = set;
const wrappedSet = (partial, replace) => {
// 验证状态更新
const newState = typeof partial === 'function'
? partial(get())
: partial;
const validation = schema.safeParse({ ...get(), ...newState });
if (!validation.success) {
console.error('State validation failed:', validation.error);
throw new Error('Invalid state update');
}
return originalSet(partial, replace);
};
return config(wrappedSet, get, api);
};
// 使用
import { z } from 'zod';
const storeSchema = z.object({
count: z.number().min(0),
user: z.object({
id: z.string(),
name: z.string().min(1)
}).nullable()
});
const useStore = create(
validationMiddleware(storeSchema)((set) => ({
count: 0,
user: null,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: Math.max(0, state.count - 1) }))
}))
);
```
### 示例 3:性能监控中间件
```javascript
const performanceMiddleware = (config) => (set, get, api) => {
const originalSet = set;
const renderCounts = {};
const wrappedSet = (partial, replace) => {
const startTime = performance.now();
const result = originalSet(partial, replace);
const endTime = performance.now();
const duration = endTime - startTime;
if (duration > 10) {
console.warn(`Slow state update: ${duration.toFixed(2)}ms`, partial);
}
return result;
};
const store = config(wrappedSet, get, api);
// 跟踪组件渲染次数
const originalSubscribe = api.subscribe;
api.subscribe = (listener, selector) => {
const wrappedListener = (state, previousState) => {
const key = selector ? selector.toString() : 'full-store';
renderCounts[key] = (renderCounts[key] || 0) + 1;
if (renderCounts[key] % 10 === 0) {
console.log(`Render count for ${key}:`, renderCounts[key]);
}
listener(state, previousState);
};
return originalSubscribe(wrappedListener, selector);
};
return store;
};
// 使用
const useStore = create(
performanceMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))
);
```
### 示例 4:撤销/重做中间件
```javascript
const undoRedoMiddleware = (config) => (set, get, api) => {
let history = [];
let future = [];
const MAX_HISTORY = 50;
const originalSet = set;
const wrappedSet = (partial, replace) => {
const previousState = get();
const result = originalSet(partial, replace);
const nextState = get();
// 保存到历史记录
history.push(previousState);
if (history.length > MAX_HISTORY) {
history.shift();
}
// 清空未来记录
future = [];
return result;
};
const store = config(wrappedSet, get, api);
// 添加撤销功能
store.undo = () => {
if (history.length === 0) return;
const previousState = history.pop();
future.push(get());
originalSet(previousState, true);
};
// 添加重做功能
store.redo = () => {
if (future.length === 0) return;
const nextState = future.pop();
history.push(get());
originalSet(nextState, true);
};
// 清空历史
store.clearHistory = () => {
history = [];
future = [];
};
return store;
};
// 使用
const useStore = create(
undoRedoMiddleware((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}))
);
// 在组件中使用
function Counter() {
const { count, increment, decrement } = useStore();
const undo = useStore((state) => state.undo);
const redo = useStore((state) => state.redo);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={undo}>Undo</button>
<button onClick={redo}>Redo</button>
</div>
);
}
```
### 关键点:
* 自定义中间件是一个高阶函数,接收 config 并返回新的配置函数
* 可以包装 set、get 和 api 来增强功能
* 中间件的执行顺序很重要,通常外层中间件先执行
* 可以在中间件中添加额外的功能,如日志、验证、性能监控等
* 中间件可以返回增强后的 store,添加新的方法或属性
服务端 · 3月7日 12:01
如何在 Zustand 中处理异步操作?### 在 Zustand 中处理异步操作的方法:
1. **基本异步操作**:
```javascript
import { create } from 'zustand';
const useStore = create((set, get) => ({
// 状态
user: null,
loading: false,
error: null,
// 异步操作
fetchUser: async (userId) => {
try {
set({ loading: true, error: null });
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
set({ user: userData, loading: false });
} catch (err) {
set({ error: err.message, loading: false });
}
},
// 另一种方式:使用 get 获取最新状态
updateUserProfile: async (updates) => {
try {
set({ loading: true, error: null });
const currentUser = get().user;
const response = await fetch(`/api/users/${currentUser.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
const updatedUser = await response.json();
set({ user: updatedUser, loading: false });
} catch (err) {
set({ error: err.message, loading: false });
}
}
}));
```
2. **使用 Promise 链**:
```javascript
const useStore = create((set) => ({
data: null,
status: 'idle', // idle, loading, success, error
fetchData: () => {
set({ status: 'loading' });
return fetch('/api/data')
.then((response) => response.json())
.then((data) => {
set({ data, status: 'success' });
return data;
})
.catch((error) => {
set({ error: error.message, status: 'error' });
throw error;
});
}
}));
```
3. **结合 React Query 或 SWR**:
```javascript
// 可以在 Zustand 中存储查询结果,同时使用 React Query 处理缓存和失效
import { create } from 'zustand';
import { useQuery } from 'react-query';
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}));
// 在组件中
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery(
['user', userId],
() => fetch(`/api/users/${userId}`).then(res => res.json())
);
// 当查询成功时,更新 Zustand store
React.useEffect(() => {
if (data) {
useStore.getState().setUser(data);
}
}, [data]);
// 使用 Zustand 中的用户数据
const user = useStore(state => state.user);
return (
<div>
{isLoading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{user && <p>User: {user.name}</p>}
</div>
);
}
```
### 关键点:
* Zustand 支持直接在 store 方法中使用 async/await
* 可以在异步操作中管理 loading 和 error 状态
* 使用 `get()` 获取最新状态,避免闭包陷阱
* 可以返回 Promise 以便在组件中处理异步操作的结果
* 可以与 React Query 或 SWR 等库结合使用,获得更好的缓存和失效策略
服务端 · 3月7日 11:48
Zustand 中级面试题:如何对 Zustand store 进行单元测试?对 Zustand store 进行单元测试相对简单,因为 store 是纯 JavaScript 对象。
### 基本测试示例:
```javascript
// store.js
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
user: null,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null })
}));
export default useStore;
```
```javascript
// store.test.js
import { renderHook, act } from '@testing-library/react';
import useStore from './store';
describe('Zustand Store', () => {
beforeEach(() => {
// 每个测试前重置 store
useStore.setState({ count: 0, user: null });
});
test('should initialize with default values', () => {
const { result } = renderHook(() => useStore());
expect(result.current.count).toBe(0);
expect(result.current.user).toBeNull();
});
test('should increment count', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement count', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
test('should set user', () => {
const { result } = renderHook(() => useStore());
const mockUser = { id: 1, name: 'John' };
act(() => {
result.current.setUser(mockUser);
});
expect(result.current.user).toEqual(mockUser);
});
test('should reset store', () => {
const { result } = renderHook(() => useStore());
const mockUser = { id: 1, name: 'John' };
act(() => {
result.current.setUser(mockUser);
result.current.increment();
});
expect(result.current.count).toBe(1);
expect(result.current.user).toEqual(mockUser);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(0);
expect(result.current.user).toBeNull();
});
});
```
### 测试异步操作:
```javascript
// store.js
const useStore = create((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (userId) => {
try {
set({ loading: true, error: null });
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
set({ user: userData, loading: false });
} catch (err) {
set({ error: err.message, loading: false });
}
}
}));
```
```javascript
// store.test.js
import { renderHook, act, waitFor } from '@testing-library/react';
import useStore from './store';
describe('Zustand Store - Async Operations', () => {
beforeEach(() => {
useStore.setState({ user: null, loading: false, error: null });
});
test('should fetch user successfully', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: 1, name: 'John' })
})
);
const { result } = renderHook(() => useStore());
await act(async () => {
await result.current.fetchUser(1);
});
expect(result.current.user).toEqual({ id: 1, name: 'John' });
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
test('should handle fetch error', async () => {
global.fetch = jest.fn(() => Promise.reject(new Error('Network error')));
const { result } = renderHook(() => useStore());
await act(async () => {
await result.current.fetchUser(1);
});
expect(result.current.error).toBe('Network error');
expect(result.current.loading).toBe(false);
});
});
```
### 测试选择性订阅:
```javascript
import { renderHook } from '@testing-library/react';
import useStore from './store';
describe('Zustand Store - Selective Subscription', () => {
beforeEach(() => {
useStore.setState({ count: 0, user: null });
});
test('should only re-render when subscribed state changes', () => {
const renderCount = jest.fn();
const { result } = renderHook(() => {
renderCount();
return useStore((state) => state.count);
});
expect(renderCount).toHaveBeenCalledTimes(1);
act(() => {
useStore.getState().setUser({ id: 1, name: 'John' });
});
// 不应该重新渲染,因为 user 变化,但订阅的是 count
expect(renderCount).toHaveBeenCalledTimes(1);
act(() => {
useStore.getState().increment();
});
// 应该重新渲染,因为 count 变化
expect(renderCount).toHaveBeenCalledTimes(2);
});
});
```
### 关键点:
* 使用 `@testing-library/react` 的 `renderHook` 和 `act` 进行测试
* 在每个测试前重置 store 状态
* 对于异步操作,使用 `waitFor` 等待状态更新
* 测试选择性订阅时,验证重渲染次数
* 使用 `useStore.getState()` 直接访问和操作 store
服务端 · 3月7日 11:44