1. React Hooks 革命:函数式组件的崛起
2019年2月,React 16.8版本正式引入Hooks特性,这可能是近年来前端开发领域最具颠覆性的变革之一。作为一名经历过jQuery时代、AngularJS时期,最终在React生态扎根的前端开发者,我清晰地记得第一次使用useState时那种"原来状态管理可以这么简单"的震撼感。
1.1 类组件的时代痛点
在Hooks出现之前,我们不得不面对类组件的诸多限制:
- 生命周期方法的割裂:componentDidMount和componentDidUpdate中经常出现重复代码
- 逻辑复用的困境:高阶组件(HOC)导致组件层级嵌套过深,render props模式使代码难以维护
- this绑定问题:忘记绑定事件处理器导致this指向错误是常见bug来源
- 代码组织混乱:相关逻辑分散在不同生命周期方法中
我曾维护过一个电商项目的购物车组件,其类组件版本包含超过600行代码,状态管理和副作用逻辑分散在8个不同的生命周期方法中。每次修改都需要在多个方法间跳转,测试覆盖率难以提升。
1.2 函数式组件的进化
Hooks的核心理念很简单:让函数组件拥有自己的状态和生命周期能力。这种转变带来了几个显著优势:
- 逻辑复用更简单:自定义Hook可以提取任何状态逻辑,无需修改组件结构
- 代码组织更合理:相关代码可以集中在一起,而非按生命周期方法强制拆分
- 学习成本更低:不需要理解JavaScript类的复杂概念(this、super等)
- 类型支持更好:函数组件对TypeScript的支持更自然
根据我在多个项目中的实测,将类组件重构为函数式组件后,代码量平均减少30%-40%,同时单元测试编写效率提升约50%。
2. React Hooks 核心机制深度解析
2.1 Hooks 的工作原理
React Hooks的实现基于几个关键设计:
2.1.1 链表存储结构
每个函数组件对应一个"记忆单元"链表,每次调用Hook都会在链表中创建一个新节点。这就是为什么Hook必须在顶层调用——React依赖调用顺序来追踪状态。
javascript复制// 简化的Hook链表实现示意
let currentHook = null;
let firstHook = null;
let nextHook = null;
function useState(initialValue) {
const hook = {
state: initialValue,
queue: [],
next: null
};
if (!firstHook) {
firstHook = hook;
} else {
currentHook.next = hook;
}
currentHook = hook;
// ...省略dispatch逻辑
}
2.1.2 调度与更新机制
当调用状态更新函数时,React会将更新加入队列,并触发重新渲染。在下次渲染时,React会按顺序遍历Hook链表,应用所有排队中的更新。
关键细节:React使用"双缓冲"技术维护两个Hook链表——一个用于当前渲染,一个用于正在构建的工作进度。这确保了并发模式下的状态一致性。
2.2 useState 的进阶用法
2.2.1 惰性初始状态
对于计算量大的初始值,可以传递函数作为初始参数:
javascript复制const [data, setData] = useState(() => {
// 这个复杂计算只会在初始渲染时执行一次
return computeExpensiveInitialState(props);
});
2.2.2 函数式更新
当新状态依赖旧状态时,应该使用函数式更新:
javascript复制// 正确做法
setCount(prevCount => prevCount + 1);
// 潜在问题:如果连续调用多次,可能不会按预期工作
setCount(count + 1);
setCount(count + 1); // 这两次调用会使用相同的count值
2.2.3 状态批处理
React 18默认启用自动批处理,这意味着多个状态更新会被合并为单个重新渲染:
javascript复制function handleClick() {
setName('Alice');
setAge(30);
// 在React 18中,这只会导致一次重新渲染
}
2.3 useEffect 的精确控制
2.3.1 依赖数组的深入理解
依赖数组决定了effect何时重新执行,但过度依赖可能导致无限循环:
javascript复制useEffect(() => {
// 这个effect会在每次渲染后都执行
});
useEffect(() => {
// 这个effect只会在mount时执行一次
}, []);
useEffect(() => {
// 这个effect在count变化时执行
}, [count]);
常见陷阱:在依赖数组中遗漏依赖项,特别是函数:
javascript复制const fetchData = useCallback(() => {
// 获取数据
}, [query]);
useEffect(() => {
fetchData();
}, []); // ❌ 遗漏了fetchData依赖
2.3.2 清理函数的必要性
任何设置了订阅、定时器或事件监听的effect都应该返回清理函数:
javascript复制useEffect(() => {
const timer = setInterval(() => {
// 定时任务
}, 1000);
return () => clearInterval(timer); // 清理定时器
}, []);
2.3.3 竞态条件处理
在数据获取场景中,必须处理组件卸载后响应到达的情况:
javascript复制useEffect(() => {
let isMounted = true;
fetch(url).then(response => {
if (isMounted) {
setData(response.data);
}
});
return () => {
isMounted = false;
};
}, [url]);
3. 高级Hook模式与性能优化
3.1 useReducer 的灵活应用
3.1.1 复杂状态管理
当状态逻辑复杂时,useReducer比useState更合适:
javascript复制function todosReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, action.payload];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? {...todo, completed: !todo.completed} : todo
);
case 'REMOVE':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
// ...
}
3.1.2 与Context结合
useReducer + useContext可以创建轻量级的状态管理方案:
javascript复制const TodosContext = createContext();
function TodosProvider({children}) {
const [state, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext.Provider value={{state, dispatch}}>
{children}
</TodosContext.Provider>
);
}
function useTodos() {
return useContext(TodosContext);
}
3.2 useMemo 与 useCallback 的性能优化
3.2.1 计算缓存
useMemo缓存计算结果,避免不必要的重复计算:
javascript复制const sortedList = useMemo(() => {
return largeList.sort((a, b) => a.value - b.value);
}, [largeList]); // 只有当largeList变化时才重新排序
3.2.2 函数引用稳定
useCallback保持函数引用稳定,避免子组件不必要的重渲染:
javascript复制const handleClick = useCallback(() => {
// 处理点击
}, [dependency]); // 只有当dependency变化时才会创建新函数
3.2.3 优化实践建议
- 不要过早优化:只有在性能问题确实存在时才使用这些Hook
- 正确设置依赖:遗漏依赖可能导致bug,过多依赖可能使优化失效
- 配合React.memo:useCallback通常与React.memo一起使用才有意义
3.3 useRef 的多用途模式
3.3.1 访问DOM元素
最常见的用法是获取DOM引用:
javascript复制function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦输入框</button>
</>
);
}
3.3.2 存储可变值
useRef可以存储任何可变值,且变更不会触发重新渲染:
javascript复制function Timer() {
const countRef = useRef(0); // 不会触发重新渲染
useEffect(() => {
const timer = setInterval(() => {
countRef.current += 1;
}, 1000);
return () => clearInterval(timer);
}, []);
// ...
}
3.3.3 保存上一次的值
实现获取上一次props或state的模式:
javascript复制function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
4. 自定义Hook的设计模式
4.1 自定义Hook的设计原则
4.1.1 命名约定
自定义Hook名称必须以"use"开头,这是React识别Hook的约定:
javascript复制// 正确
function useLocalStorage(key, initialValue) {}
// 错误
function getLocalStorage(key, initialValue) {}
4.1.2 单一职责
每个Hook应该只关注一个特定功能:
javascript复制// 不好:处理太多事情
function useUserData() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 获取用户数据
// 更新用户数据
// 处理用户权限
// ...
}
// 更好:拆分为多个Hook
function useFetchUser(userId) {}
function useUpdateUser() {}
function useUserPermissions() {}
4.1.3 清晰的输入输出
定义明确的参数和返回值类型:
typescript复制interface UsePaginationOptions<T> {
initialPage?: number;
pageSize?: number;
data: T[];
}
interface UsePaginationResult<T> {
currentPage: number;
pageData: T[];
totalPages: number;
goToPage: (page: number) => void;
nextPage: () => void;
prevPage: () => void;
}
function usePagination<T>(options: UsePaginationOptions<T>): UsePaginationResult<T> {
// 实现...
}
4.2 复杂Hook设计示例
4.2.1 useAsync:通用异步操作管理
javascript复制function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(() => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction()
.then(response => {
setValue(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
}
// 使用示例
function UserProfile({ userId }) {
const { value: user, status, error } = useAsync(
() => fetchUser(userId),
true
);
if (status === 'idle' || status === 'pending') {
return <div>加载中...</div>;
}
if (status === 'error') {
return <div>错误: {error.message}</div>;
}
return <div>{user.name}</div>;
}
4.2.2 useUndo:实现撤销/重做功能
javascript复制function useUndo(initialPresent) {
const [state, setState] = useState({
past: [],
present: initialPresent,
future: []
});
const canUndo = state.past.length !== 0;
const canRedo = state.future.length !== 0;
const undo = useCallback(() => {
setState(currentState => {
if (currentState.past.length === 0) {
return currentState;
}
const previous = currentState.past[currentState.past.length - 1];
const newPast = currentState.past.slice(0, -1);
return {
past: newPast,
present: previous,
future: [currentState.present, ...currentState.future]
};
});
}, []);
const redo = useCallback(() => {
setState(currentState => {
if (currentState.future.length === 0) {
return currentState;
}
const next = currentState.future[0];
const newFuture = currentState.future.slice(1);
return {
past: [...currentState.past, currentState.present],
present: next,
future: newFuture
};
});
}, []);
const set = useCallback(newPresent => {
setState(currentState => {
if (newPresent === currentState.present) {
return currentState;
}
return {
past: [...currentState.past, currentState.present],
present: newPresent,
future: []
};
});
}, []);
const reset = useCallback(newPresent => {
setState({
past: [],
present: newPresent,
future: []
});
}, []);
return [state.present, { set, reset, undo, redo, canUndo, canRedo }];
}
// 使用示例
function Counter() {
const [count, { set: setCount, undo, redo, canUndo, canRedo }] = useUndo(0);
return (
<div>
<p>当前值: {count}</p>
<button onClick={() => setCount(count + 1)}>加一</button>
<button onClick={() => setCount(count - 1)}>减一</button>
<button onClick={undo} disabled={!canUndo}>撤销</button>
<button onClick={redo} disabled={!canRedo}>重做</button>
</div>
);
}
4.3 Hook组合模式
4.3.1 构建复杂逻辑
通过组合简单Hook来构建复杂功能:
javascript复制function useUserDashboard(userId) {
const user = useFetchUser(userId);
const orders = useFetchOrders(userId);
const notifications = useFetchNotifications(userId);
const [activeTab, setActiveTab] = useState('overview');
const isLoading = user.loading || orders.loading || notifications.loading;
const error = user.error || orders.error || notifications.error;
return {
isLoading,
error,
user: user.data,
orders: orders.data,
notifications: notifications.data,
activeTab,
setActiveTab
};
}
4.3.2 依赖注入模式
使Hook更灵活,通过参数注入依赖:
javascript复制function useAPI(endpoint, options = {}) {
const { fetch = window.fetch, ...fetchOptions } = options;
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await fetch(endpoint, {
...fetchOptions,
signal: controller.signal
});
if (!response.ok) throw new Error(response.statusText);
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [endpoint, fetch, JSON.stringify(fetchOptions)]);
return { data, loading, error };
}
// 使用时可注入自定义fetch实现
const { data } = useAPI('/users', {
fetch: myCustomFetchImplementation
});
5. React与Vue 3 Hooks机制对比
5.1 核心设计哲学差异
5.1.1 React的函数式思想
React Hooks基于纯函数和不可变数据:
- 状态更新触发重新渲染
- 依赖显式声明
- 强调不可变性和纯函数
javascript复制// React典型模式
function Counter() {
const [count, setCount] = useState(0);
// 显式声明effect依赖
useEffect(() => {
document.title = `计数: ${count}`;
}, [count]);
return (
<button onClick={() => setCount(c => c + 1)}>
点击次数: {count}
</button>
);
}
5.1.2 Vue的响应式思想
Vue Composition API基于可变状态和自动依赖追踪:
- 响应式系统自动追踪依赖
- 状态是可变的
- 强调声明式和响应式
javascript复制// Vue典型模式
<script setup>
import { ref, watchEffect } from 'vue';
const count = ref(0);
// 自动追踪依赖
watchEffect(() => {
document.title = `计数: ${count.value}`;
});
</script>
<template>
<button @click="count++">
点击次数: {{ count }}
</button>
</template>
5.2 性能优化策略对比
5.2.1 React的优化手段
- React.memo:记忆组件,防止不必要的重新渲染
- useMemo/useCallback:缓存计算结果和函数引用
- 状态精细化:将状态拆分为更小的单元
javascript复制// React优化示例
const ExpensiveComponent = React.memo(({ list }) => {
const sortedList = useMemo(() => {
return [...list].sort();
}, [list]);
return <div>{sortedList.join(', ')}</div>;
});
5.2.2 Vue的优化手段
- computed:自动缓存计算属性
- shallowRef:非深度响应式引用
- 手动控制响应式:使用markRaw等API
javascript复制// Vue优化示例
const sortedList = computed(() => {
return [...props.list].sort();
});
5.3 开发体验对比
5.3.1 React的优势
- 更灵活的代码组织:可以自由组合各种Hook
- 更丰富的生态:社区提供了大量自定义Hook
- 更明确的依赖控制:显式声明依赖关系
5.3.2 Vue的优势
- 更简洁的语法:自动依赖追踪减少样板代码
- 更自然的响应式:直接修改值即可触发更新
- 更好的类型推断:与TypeScript集成更顺畅
5.4 迁移与互操作建议
5.4.1 从React到Vue
- 将useState替换为ref或reactive
- 将useEffect替换为watch或watchEffect
- 将useMemo替换为computed
- 注意Vue的响应式解包特性
5.4.2 从Vue到React
- 将ref/reactive替换为useState/useReducer
- 将watch/watchEffect替换为useEffect
- 将computed替换为useMemo
- 注意显式声明所有依赖
6. 实战经验与性能调优
6.1 常见性能问题与解决方案
6.1.1 不必要的重新渲染
问题表现:组件频繁重新渲染,即使props/state没有实质性变化
解决方案:
- 使用React.memo包装组件
- 确保传递的props是稳定的(使用useMemo/useCallback)
- 精细化状态管理,避免大对象状态
javascript复制// 优化前:每次父组件渲染都会导致子组件重新渲染
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('点击');
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>增加</button>
<Child onClick={handleClick} />
</>
);
}
// 优化后:使用React.memo和useCallback
const Child = React.memo(function Child({ onClick }) {
return <button onClick={onClick}>子组件</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('点击');
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>增加</button>
<Child onClick={handleClick} />
</>
);
}
6.1.2 昂贵的计算
问题表现:组件渲染时执行复杂计算导致界面卡顿
解决方案:
- 使用useMemo缓存计算结果
- 考虑Web Worker处理CPU密集型任务
- 实现虚拟滚动或分页加载大数据集
javascript复制// 优化前:每次渲染都重新计算
function List({ items }) {
const sortedItems = [...items].sort((a, b) => a.value - b.value);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// 优化后:使用useMemo
function List({ items }) {
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.value - b.value);
}, [items]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
6.2 测试策略与技巧
6.2.1 单元测试自定义Hook
使用@testing-library/react-hooks测试自定义Hook:
javascript复制import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('应该增加计数', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
6.2.2 组件集成测试
测试使用Hook的组件:
javascript复制import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('点击按钮应该增加计数', () => {
render(<Counter />);
const button = screen.getByText(/点击次数/);
fireEvent.click(button);
expect(button.textContent).toBe('点击次数: 1');
});
6.3 调试技巧与工具
6.3.1 React DevTools
- 使用"Hooks"面板检查Hook值和依赖
- 使用"Profiler"识别性能瓶颈
- 检查Hook调用顺序是否正确
6.3.2 自定义调试Hook
创建useDebugHook辅助开发:
javascript复制function useDebugHook(name, value) {
useEffect(() => {
console.log(`${name} 更新:`, value);
}, [name, value]);
}
// 使用示例
function MyComponent({ id }) {
const [count, setCount] = useState(0);
useDebugHook('count', count);
// ...
}
6.3.3 错误边界处理
使用ErrorBoundary捕获Hook中的错误:
javascript复制class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// 使用示例
<ErrorBoundary fallback={<div>出错了</div>}>
<ComponentWithHooks />
</ErrorBoundary>
7. 架构设计与最佳实践
7.1 项目结构组织
7.1.1 Hook分类存储
推荐的项目结构:
code复制src/
hooks/
api/ # 数据获取相关Hook
useFetch.js
useMutation.js
ui/ # UI相关Hook
useModal.js
useTooltip.js
state/ # 状态管理Hook
useLocalStorage.js
useUndo.js
utils/ # 工具类Hook
useDebounce.js
useInterval.js
index.js # 统一导出入口
7.1.2 文档规范
为每个自定义Hook添加JSDoc注释:
javascript复制/**
* 自定义Hook用于管理本地存储数据
* @param {string} key 存储键名
* @param {any} initialValue 初始值
* @returns {[any, (value: any) => void]} 返回存储值和更新函数
*/
function useLocalStorage(key, initialValue) {
// 实现...
}
7.2 类型安全(TypeScript)
7.2.1 基本类型定义
为自定义Hook添加完整类型:
typescript复制interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
function useFetch<T = unknown>(url: string): UseFetchResult<T> {
// 实现...
}
7.2.2 泛型应用
创建灵活的泛型Hook:
typescript复制function usePagination<T>(items: T[], itemsPerPage: number) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(items.length / itemsPerPage);
const paginatedItems = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
return items.slice(start, end);
}, [items, currentPage, itemsPerPage]);
return {
currentPage,
paginatedItems,
totalPages,
goToPage: setCurrentPage
};
}
7.3 性能优化策略
7.3.1 按需加载Hook
使用React.lazy和Suspense懒加载重型Hook:
javascript复制const useHeavyHook = React.lazy(() => import('./hooks/useHeavyHook'));
function Component() {
return (
<React.Suspense fallback={<div>加载中...</div>}>
<InnerComponent />
</React.Suspense>
);
}
function InnerComponent() {
const heavyData = useHeavyHook();
// ...
}
7.3.2 上下文优化
避免在Context中传递频繁变化的值:
javascript复制// 不好:传递的对象包含频繁变化的值
<UserContext.Provider value={{ user, setUser }}>
{/* 子组件 */}
</UserContext.Provider>
// 更好:拆分Context
<SetUserContext.Provider value={setUser}>
<UserContext.Provider value={user}>
{/* 子组件 */}
</UserContext.Provider>
</SetUserContext.Provider>
7.4 团队协作规范
7.4.1 代码审查要点
审查自定义Hook时检查:
- 是否遵循Hook规则(顶层调用、正确依赖)
- 是否有适当的清理逻辑
- 错误处理是否完善
- 性能优化是否合理
- 类型定义是否完整(TypeScript项目)
7.4.2 文档共享
建立团队Hook文档,包含:
- 每个Hook的用途和基本用法
- 参数和返回值说明
- 常见使用场景示例
- 已知限制和注意事项
8. 未来演进与替代方案
8.1 React Hooks的未来发展
8.1.1 use Hook (RFC)
React团队正在讨论的use Hook,用于更简单地处理异步资源和Context:
javascript复制// 提案中的用法
function Note({ id }) {
const note = use(fetchNote(id));
return (
<div>
<h1>{note.title}</h1>
<p>{note.content}</p>
</div>
);
}
8.1.2 编译器优化
React团队正在开发编译器优化,可能减少对手动记忆化(useMemo/useCallback)的需求。
8.2 状态管理替代方案
8.2.1 Zustand
轻量级状态管理库,基于Hook的API:
javascript复制import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 }))
}));
function Counter() {
const { count, increment } = useStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
8.2.2 Jotai
原子状态管理,类似Recoil但更简单:
javascript复制import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
8.3 服务端组件与React Hooks
React 18引入的服务端组件(RSC)可能改变Hook的使用方式:
- 服务端组件不能使用Hook
- 客户端组件仍然可以自由使用Hook
- 需要重新思考状态管理和数据获取的架构
javascript复制// 服务端组件
async function ServerComponent() {
const data = await fetchData();
return (
<div>
<ClientComponent data={data} />
</div>
);
}
// 客户端组件
'use client';
function ClientComponent({ data }) {
const [count, setCount] = useState(0);
return (
<div>
<p>服务端数据: {data.title}</p>
<p>客户端状态: {count}</p>
</div>
);
}
9. 从理论到实践:完整案例研究
9.1 电商购物车实现
9.1.1 需求分析
- 显示商品列表,可调整数量
- 计算总价和折扣
- 保存到本地存储
- 支持优惠码应用
9.1.2 Hook设计与实现
javascript复制function useShoppingCart(initialItems = []) {
const [items, setItems] = useLocalStorage('cart', initialItems);
const [coupon, setCoupon] = useState(null);
const addItem = useCallback((product) => {
setItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
}, []);
const removeItem = useCallback((productId) => {
setItems(prev => prev.filter(item => item.id !== productId));
}, []);
const updateQuantity = useCallback((productId, quantity) => {
setItems(prev =>
prev.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
}, []);
const subtotal = useMemo(() => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [items]);
const discount = useMemo(() => {
if (!coupon) return 0;
return coupon.type === 'percentage'
? subtotal * coupon.value / 100
: coupon.value;
}, [coupon, subtotal]);
const total = subtotal - discount;
const applyCoupon = useCallback((code) => {
// 模拟API验证
return validateCoupon(code).then(validCoupon => {
setCoupon(validCoupon);
return validCoupon;
});
}, []);
return {
items,
addItem,
removeItem,
updateQuantity,
subtotal,
discount,
total,
coupon,
applyCoupon
};
}
// 使用示例
function ShoppingCart() {
const {
items,
addItem,
removeItem,
updateQuantity,
subtotal,
discount,
total
} = useShoppingCart();
// 渲染购物车UI
}
9.2 实时协作编辑器
9.2.1 需求分析
- 实时同步编辑器内容
- 显示其他在线用户
- 处理冲突解决
- 离线支持
9.2.2 Hook设计与实现
javascript复制function useCollaborativeEditor(docId, userId) {
const [content, setContent] = useLocalStorage(`doc-${docId}`, '');
const [users, setUsers] = useState([]);
const [connection, setConnection] = useState(null);
// 连接WebSocket服务器
useEffect(() => {
const socket = new WebSocket(`wss://api.example.com/docs/${docId}`);
socket.onopen = () => {
socket.send(JSON.stringify({ type: 'join', userId }));
setConnection(socket);
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'update':
setContent(message.content);
break;
case 'users':
setUsers(message.users);
break;
}
};
return () => {
socket.close();
};
}, [docId, userId]);
// 发送更新到服务器
const handleChange = useDebounce((newContent) => {
if (connection) {
connection.send(JSON.stringify({
type: 'update',
content: newContent,
userId
}));
}
setContent(newContent);
}, 300);
return {
content,
users,
handleChange
};
}
// 使用示例
function Editor({ docId, userId }) {
const { content, users, handleChange } = useCollaborativeEditor(docId, userId);
return (
<div>
<div>在线用户: {users.join(', ')}</div>
<textarea
value={content}
onChange={(e) => handleChange(e.target.value)}
/>
</div>
);
}
10. 专家级技巧与模式
10.1 高级Hook组合模式
10.1.1 Hook工厂函数
创建可配置的Hook生成器:
javascript复制function createUseLocalStorage(key, initialValue) {
return function useGeneratedHook() {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);
return [value, setValue];
};
}
// 使用工厂创建特定Hook
const useTheme =