乐闻世界logo
搜索文章和话题

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 是一个非常不错的选择。
React
查看更多相关内容
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