𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

React Hooks完全使用指南:从入门到精通

自从React Hooks在2019年发布以来,它彻底改变了我们编写React组件的方式。如果你还在使用class组件,那么现在正是转向Hooks的最佳时机。今天,我将带你全面掌握React Hooks的使用技巧和最佳实践。

目录


Hooks简介

什么是Hooks?

Hooks是React 16.8引入的新特性,它允许你在函数组件中使用状态和其他React特性。

为什么需要Hooks?

在Hooks出现之前,React有两种组件类型:

  1. 函数组件:无状态组件,只能用于展示UI
  2. 类组件:有状态组件,可以使用生命周期、状态管理等

类组件的缺点:

  • 代码冗长
  • 难以复用状态逻辑
  • 复杂组件难以理解和维护

Hooks解决了这些问题,让函数组件也能使用所有React特性。

Hooks的基本规则

  1. 只在最顶层调用Hooks:不要在循环、条件或嵌套函数中调用Hooks
  2. 只在React函数中调用Hooks:不要在普通JavaScript函数中调用Hooks
// ✅ 正确的Hook使用
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 副作用
}, [count]);
return <div>{count}</div>;
}

// ❌ 错误的Hook使用
function MyComponent() {
if (condition) {
const [count, setCount] = useState(0); // 不能在条件语句中使用
}

for (let i = 0; i < 3; i++) {
const [count, setCount] = useState(0); // 不能在循环中使用
}

function innerFunction() {
const [count, setCount] = useState(0); // 不能在嵌套函数中使用
}

return <div>...</div>;
}

Hooks的分类

React Hooks可以分为以下几类:

类型用途示例
核心Hooks处理状态和副作用useState, useEffect, useContext
额外Hooks处理特殊场景useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue
自定义Hooks复用组件逻辑自定义Hook函数

核心Hooks详解

useState

useState是最基础的Hook,用于在函数组件中添加状态。

import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}

useState的高级用法

  1. 使用函数更新状态

    // 避免闭包问题
    setCount(prevCount => prevCount + 1);
  2. 使用对象状态

    const [user, setUser] = useState({
    name: 'John',
    age: 25
    });

    // 更新对象
    setUser(prevUser => ({
    ...prevUser,
    age: 30
    }));
  3. 使用数组状态

    const [items, setItems] = useState([]);

    // 添加项目
    setItems(prevItems => [...prevItems, 'New Item']);

    // 过滤项目
    setItems(prevItems => prevItems.filter(item => item !== 'Old Item'));
  4. 状态初始化函数

    function expensiveInitialization() {
    console.log('Initializing state...');
    return Math.random();
    }

    const [value, setValue] = useState(expensiveInitialization);
    // 只在组件挂载时执行一次
  5. 读取最新状态

    function Timer() {
    const [count, setCount] = useState(0);
    const intervalRef = useRef();

    useEffect(() => {
    intervalRef.current = setInterval(() => {
    setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(intervalRef.current);
    }, []);

    return <div>{count}</div>;
    }

useEffect

useEffect用于处理副作用,相当于类组件中的componentDidMountcomponentDidUpdatecomponentWillUnmount

import React, { useState, useEffect } from 'react';

function DataFetcher() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 组件挂载时执行
fetchData();

return () => {
// 组件卸载时执行
console.log('Component unmounted');
};
}, []); // 空依赖数组,只在挂载和卸载时执行

const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
setLoading(false);
} catch (error) {
console.error('Error:', error);
setLoading(false);
}
};

if (loading) {
return <div>Loading...</div>;
}

return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}

useEffect的依赖数组

依赖数组控制effect的执行时机:

useEffect(() => {
console.log('Effect runs');
}); // 每次渲染都执行

useEffect(() => {
console.log('Effect runs on mount');
}, []); // 只在挂载时执行

useEffect(() => {
console.log('Effect runs when count changes', count);
}, [count]); // count变化时执行

useEffect(() => {
console.log('Effect runs when count or name changes');
}, [count, name]); // count或name变化时执行

useEffect的清理函数

useEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);

return () => {
clearInterval(timer); // 清理函数
};
}, []);

useContext

useContext用于在组件间共享数据,避免props drilling。

// 创建Context
const UserContext = React.createContext();

// Provider组件
function App() {
const [user, setUser] = useState({ name: 'John', age: 25 });

return (
<UserContext.Provider value={{ user, setUser }}>
<Header />
<Main />
</UserContext.Provider>
);
}

// 使用Context的组件
function Header() {
const { user } = useContext(UserContext);

return <div>Header: {user.name}</div>;
}

function Main() {
const { user, setUser } = useContext(UserContext);

return (
<div>
<div>Main: {user.name}</div>
<button onClick={() => setUser({ ...user, age: 30 })}>
Update Age
</button>
</div>
);
}

useReducer

useReducer用于处理复杂的逻辑,是useState的替代方案。

const initialState = { count: 0 };

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}

使用useReducer处理复杂状态

const initialState = {
todos: [
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build something', completed: true }
],
filter: 'all'
};

function todosReducer(state, action) {
switch (action.type) {
case 'addTodo':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case 'toggleTodo':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'setFilter':
return {
...state,
filter: action.payload
};
default:
return state;
}
}

function TodoApp() {
const [state, dispatch] = useReducer(todosReducer, initialState);
const [newTodo, setNewTodo] = useState('');

const addTodo = () => {
if (newTodo.trim()) {
dispatch({ type: 'addTodo', payload: newTodo });
setNewTodo('');
}
};

const toggleTodo = (id) => {
dispatch({ type: 'toggleTodo', payload: id });
};

const setFilter = (filter) => {
dispatch({ type: 'setFilter', payload: filter });
};

const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'completed') return todo.completed;
if (state.filter === 'active') return !todo.completed;
return true;
});

return (
<div>
<h1>Todo App</h1>
<div>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
</div>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
</div>
);
}

自定义Hooks

自定义Hooks是React Hooks最强大的特性之一,它让你可以复用组件逻辑。

创建自定义Hook的规则

  1. use开头
  2. 函数组件内部使用
  3. 可以调用其他Hooks

基础自定义Hook

1. useLocalStorage

function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});

const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};

return [storedValue, setValue];
}

// 使用示例
function UserProfile() {
const [name, setName] = useLocalStorage('user-name', 'John Doe');
const [age, setAge] = useLocalStorage('user-age', 25);

return (
<div>
<h2>Profile</h2>
<p>Name: {name}</p>
<p>Age: {age}</p>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
/>
</div>
);
}

2. useWindowSize

function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});

useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

return windowSize;
}

// 使用示例
function ResponsiveComponent() {
const { width, height } = useWindowSize();

return (
<div>
<p>Window size: {width} x {height}</p>
{width < 768 && <p>You are on mobile device</p>}
{width >= 768 && <p>You are on desktop device</p>}
</div>
);
}

3. useKeyPress

function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);

useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
};

const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};

window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);

return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);

return keyPressed;
}

// 使用示例
function KeyboardShortcuts() {
const spacePressed = useKeyPress(' ');
const enterPressed = useKeyPress('Enter');

return (
<div>
<h1>Keyboard Shortcuts</h1>
<p>Space key pressed: {spacePressed ? 'Yes' : 'No'}</p>
<p>Enter key pressed: {enterPressed ? 'Yes' : 'No'}</p>

{spacePressed && <p>You pressed Space!</p>}
{enterPressed && <p>You pressed Enter!</p>}
</div>
);
}

高级自定义Hook

1. useAsync

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 DataFetchingComponent() {
const { status, value, error, execute } = useAsync(
() => fetch('https://api.example.com/data').then(res => res.json()),
true
);

if (status === 'idle') {
return <div>Press a button to start fetching</div>;
}

if (status === 'pending') {
return <div>Loading...</div>;
}

if (status === 'error') {
return <div>Error: {error.message}</div>;
}

return (
<div>
<h2>Data:</h2>
<pre>{JSON.stringify(value, null, 2)}</pre>
<button onClick={execute}>Fetch again</button>
</div>
);
}

2. useDebounce

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;
}

// 使用示例
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);

const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);

useEffect(() => {
if (debouncedSearchTerm) {
setIsSearching(true);

fetch(`https://api.example.com/search?q=${debouncedSearchTerm}`)
.then(response => response.json())
.then(data => {
setResults(data);
setIsSearching(false);
})
.catch(error => {
console.error('Error:', error);
setIsSearching(false);
});
} else {
setResults([]);
}
}, [debouncedSearchTerm]);

return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>

{isSearching && <div>Searching...</div>}

<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}

3. useScrollPosition

function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);

useEffect(() => {
function updatePosition() {
setScrollPosition(window.pageYOffset);
}

window.addEventListener('scroll', updatePosition);
updatePosition();

return () => window.removeEventListener('scroll', updatePosition);
}, []);

return scrollPosition;
}

// 使用示例
function StickyHeader() {
const scrollPosition = useScrollPosition();

return (
<div>
<header style={{
position: scrollPosition > 100 ? 'fixed' : 'relative',
top: scrollPosition > 100 ? '0' : 'auto',
background: scrollPosition > 100 ? 'white' : 'transparent',
boxShadow: scrollPosition > 100 ? '0 2px 10px rgba(0,0,0,0.1)' : 'none',
transition: 'all 0.3s ease'
}}>
<h1>My Header</h1>
</header>

<div style={{ height: '200vh', paddingTop: '200px' }}>
<p>Scroll down to see the header change</p>
<p>Current scroll position: {scrollPosition}px</p>
</div>
</div>
);
}

Hooks最佳实践

1. 组件设计原则

保持组件简单

// ✅ 好的设计 - 单一职责
function UserProfile({ userId }) {
const user = useUser(userId);
const [isEditing, setIsEditing] = useState(false);

if (!user) return <div>Loading...</div>;

return (
<div>
{isEditing ? (
<UserProfileForm user={user} onSave={() => setIsEditing(false)} />
) : (
<UserProfileDisplay user={user} onEdit={() => setIsEditing(true)} />
)}
</div>
);
}

// ❌ 不好的设计 - 承担太多职责
function UserProfile({ userId }) {
const user = useUser(userId);
const [isEditing, setIsEditing] = useState(false);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [settings, setSettings] = useState({});

useEffect(() => {
fetchPosts(userId);
fetchComments(userId);
fetchSettings();
}, [userId]);

// ...太多逻辑
}

合并相关状态

// ✅ 合并相关状态
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
address: ''
});

function updateUser(updates) {
setUser(prev => ({ ...prev, ...updates }));
}

// ❌ 分散状态
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [address, setAddress] = useState('');

2. 状态管理最佳实践

合理使用useReducer

// ✅ 适合使用useReducer
const initialState = {
todos: [],
filter: 'all',
isLoading: false,
error: null
};

function todosReducer(state, action) {
switch (action.type) {
case 'fetchTodos':
return { ...state, isLoading: true };
case 'fetchTodosSuccess':
return { ...state, todos: action.payload, isLoading: false, error: null };
case 'fetchTodosError':
return { ...state, isLoading: false, error: action.payload };
// ...其他action
}
}

// ❌ 不适合使用useReducer
function SimpleForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
// 提交逻辑
};

return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}

使用useMemo优化性能

// ✅ 使用useMemo优化
function ExpensiveComponent({ items }) {
const expensiveValue = useMemo(() => {
console.log('Computing expensive value...');
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);

return <div>Value: {expensiveValue}</div>;
}

// ❌ 不使用useMemo
function ExpensiveComponent({ items }) {
// 每次渲染都会重新计算
const expensiveValue = items.reduce((sum, item) => sum + item.value, 0);

return <div>Value: {expensiveValue}</div>;
}

使用useCallback避免不必要的重渲染

// ✅ 使用useCallback
function ParentComponent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
console.log('Button clicked');
setCount(prev => prev + 1);
}, []); // 空依赖数组,函数不会改变

return (
<div>
<ChildComponent onButtonClick={handleClick} />
<p>Count: {count}</p>
</div>
);
}

function ChildComponent({ onButtonClick }) {
return <button onClick={onButtonClick}>Click me</button>;
}

// ❌ 不使用useCallback
function ParentComponent() {
const [count, setCount] = useState(0);

// 每次渲染都会创建新的函数
const handleClick = () => {
console.log('Button clicked');
setCount(prev => prev + 1);
};

return (
<div>
<ChildComponent onButtonClick={handleClick} />
<p>Count: {count}</p>
</div>
);
}

3. 副作用管理

useEffect的正确使用

// ✅ 正确的useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
let isMounted = true;

const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();

if (isMounted) {
setUser(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};

fetchUser();

return () => {
isMounted = false;
};
}, [userId]);

// ...渲染逻辑
}

// ❌ 错误的useEffect - 缺少依赖数组或清理函数
function UserProfile({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUser(userId);
}); // 缺少依赖数组

const fetchUser = async (id) => {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
setUser(data);
};
}

副作用的分类

// 数据获取副作用
useEffect(() => {
fetchData();
}, [dependency]);

// 事件监听副作用
useEffect(() => {
function handleResize() {
// 处理窗口大小变化
}

window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

// 订阅副作用
useEffect(() => {
const subscription = source.subscribe();
return () => subscription.unsubscribe();
}, []);

// 手动DOM操作副作用
useEffect(() => {
const element = document.getElementById('my-element');
// 操作DOM
return () => {
// 清理DOM操作
};
}, []);

4. 表单处理最佳实践

使用useReducer处理复杂表单

const initialState = {
username: '',
email: '',
password: '',
errors: {},
isSubmitting: false
};

function formReducer(state, action) {
switch (action.type) {
case 'update_field':
return {
...state,
[action.field]: action.value
};
case 'set_error':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'clear_errors':
return {
...state,
errors: {}
};
case 'set_submitting':
return {
...state,
isSubmitting: action.value
};
case 'reset':
return initialState;
default:
return state;
}
}

function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialState);

const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'clear_errors' });
dispatch({ type: 'set_submitting', value: true });

try {
// 表单验证
const errors = validateForm(state);
if (Object.keys(errors).length > 0) {
Object.entries(errors).forEach(([field, error]) => {
dispatch({ type: 'set_error', field, error });
});
return;
}

// 提交表单
await submitForm(state);
dispatch({ type: 'reset' });
} catch (error) {
console.error('Submission error:', error);
} finally {
dispatch({ type: 'set_submitting', value: false });
}
};

const handleInputChange = (e) => {
const { name, value } = e.target;
dispatch({ type: 'update_field', field: name, value });
};

return (
<form onSubmit={handleSubmit}>
<div>
<label>Username:</label>
<input
type="text"
name="username"
value={state.username}
onChange={handleInputChange}
/>
{state.errors.username && (
<div className="error">{state.errors.username}</div>
)}
</div>

<div>
<label>Email:</label>
<input
type="email"
name="email"
value={state.email}
onChange={handleInputChange}
/>
{state.errors.email && (
<div className="error">{state.errors.email}</div>
)}
</div>

<div>
<label>Password:</label>
<input
type="password"
name="password"
value={state.password}
onChange={handleInputChange}
/>
{state.errors.password && (
<div className="error">{state.errors.password}</div>
)}
</div>

<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'Submitting...' : 'Register'}
</button>
</form>
);
}

性能优化

1. 组件优化

避免不必要的渲染

// ✅ 使用React.memo优化组件
const ExpensiveItem = React.memo(function ExpensiveItem({ item, onClick }) {
console.log('Rendering ExpensiveItem');

return (
<div onClick={() => onClick(item.id)}>
{item.name} - {item.value}
</div>
);
}, (prevProps, nextProps) => {
// 自定义比较函数
return prevProps.item.id === nextProps.item.id;
});

// ❌ 不使用React.memo
function ExpensiveItem({ item, onClick }) {
console.log('Rendering ExpensiveItem');

return (
<div onClick={() => onClick(item.id)}>
{item.name} - {item.value}
</div>
);
}

使用useCallback和useMemo

function OptimizedComponent({ items, filter }) {
// 缓存过滤后的结果
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item => {
switch (filter) {
case 'active': return item.active;
case 'completed': return item.completed;
default: return true;
}
});
}, [items, filter]);

// 缓存事件处理函数
const handleItemClick = useCallback((id) => {
console.log('Item clicked:', id);
// 处理点击事件
}, []);

// 缓存计算属性
const itemCount = useMemo(() => filteredItems.length, [filteredItems]);

return (
<div>
<p>Total items: {itemCount}</p>
{filteredItems.map(item => (
<ExpensiveItem
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</div>
);
}

2. 代码分割

使用React.lazy和Suspense

import React, { Suspense, lazy } from 'react';

// 懒加载组件
const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
return (
<div>
<h1>My App</h1>

<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}

路由级别的代码分割

import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const Contact = React.lazy(() => import('./pages/Contact'));

function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}

3. 虚拟列表

使用react-window

import React, { useState, useCallback, useMemo } from 'react';
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style, data }) => (
<div style={style}>
{data[index].name} - {data[index].value}
</div>
);

function VirtualList({ items }) {
const [filter, setFilter] = useState('');

const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);

return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>

<List
height={600}
itemCount={filteredItems.length}
itemSize={35}
width={300}
>
{({ index, style }) => (
<Row index={index} style={style} data={filteredItems} />
)}
</List>
</div>
);
}

常见问题解决

1. 状态更新的问题

问题:状态更新不生效

// ❌ 问题代码
function Counter() {
const [count, setCount] = useState(0);

const handleIncrement = () => {
const newCount = count + 1;
setCount(newCount);
};

const handleDouble = () => {
const newCount = count * 2;
setCount(newCount);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleDouble}>x2</button>
</div>
);
}

解决方案:使用函数更新

// ✅ 解决方案
function Counter() {
const [count, setCount] = useState(0);

const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};

const handleDouble = () => {
setCount(prevCount => prevCount * 2);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleDouble}>x2</button>
</div>
);
}

2. 依赖数组的问题

问题:无限循环或副作用不执行

// ❌ 问题代码
function UserProfile({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUser(userId);
}, []); // 缺少userId依赖

const fetchUser = async (id) => {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
setUser(data);
};
}

解决方案:正确使用依赖数组

// ✅ 解决方案
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (!userId) return;

let isMounted = true;
setLoading(true);

const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();

if (isMounted) {
setUser(data);
}
} catch (error) {
console.error('Error:', error);
} finally {
if (isMounted) {
setLoading(false);
}
}
};

fetchUser();

return () => {
isMounted = false;
};
}, [userId]); // 添加userId依赖

// ...渲染逻辑
}

3. 性能问题

问题:组件频繁重渲染

// ❌ 问题代码
function ParentComponent() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ChildComponent />
<ChildComponent />
<ChildComponent />
</div>
);
}

function ChildComponent() {
// 每次父组件渲染都会重渲染
return <div>Child Component</div>;
}

解决方案:使用useMemo和React.memo

// ✅ 解决方案
function ParentComponent() {
const [count, setCount] = useState(0);

// 缓存计算结果
const doubledCount = useMemo(() => {
console.log('Computing doubled count...');
return count * 2;
}, [count]);

return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count} (Doubled: {doubledCount})
</button>
<React.memo>ChildComponent</React.memo>
<ChildComponent />
<ChildComponent />
</div>
);
}

const ChildComponent = React.memo(function ChildComponent() {
return <div>Child Component</div>;
});

4. 自定义Hook问题

问题:自定义Hook违反规则

// ❌ 问题代码
function useCustomHook() {
const [count, setCount] = useState(0);

if (condition) {
// 在条件语句中使用Hook,违反规则
const [data, setData] = useState([]);
}

return { count };
}

解决方案:正确使用自定义Hook

// ✅ 解决方案
function useCustomHook() {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);

// 使用条件逻辑但不违反Hook规则
useEffect(() => {
if (condition) {
fetchData();
}
}, [condition]);

const fetchData = async () => {
const response = await fetch('api/data');
const result = await response.json();
setData(result);
};

return { count, data };
}

实战案例

案例1:实现一个可复用的数据表格组件

import React, { useState, useEffect, useMemo, useCallback } from 'react';

function useFetchData(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const controller = new AbortController();

const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
signal: controller.signal
});

if (!response.ok) {
throw new Error('Network response was not ok');
}

const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};

fetchData();

return () => {
controller.abort();
};
}, [url]);

return { data, loading, error };
}

function usePagination(items, itemsPerPage = 10) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(items.length / itemsPerPage);

const currentItems = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
return items.slice(startIndex, startIndex + itemsPerPage);
}, [items, currentPage, itemsPerPage]);

const goToPage = useCallback((page) => {
setCurrentPage(page);
}, []);

const goToNextPage = useCallback(() => {
setCurrentPage(prev => Math.min(prev + 1, totalPages));
}, [totalPages]);

const goToPrevPage = useCallback(() => {
setCurrentPage(prev => Math.max(prev - 1, 1));
}, []);

return {
currentItems,
currentPage,
totalPages,
goToPage,
goToNextPage,
goToPrevPage
};
}

function DataTable({ url, columns, itemsPerPage = 10 }) {
const { data, loading, error } = useFetchData(url);
const {
currentItems,
currentPage,
totalPages,
goToPage,
goToNextPage,
goToPrevPage
} = usePagination(data, itemsPerPage);

const [sortConfig, setSortConfig] = useState({
key: null,
direction: 'ascending'
});

const handleSort = useCallback((key) => {
let direction = 'ascending';
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({ key, direction });
}, [sortConfig]);

const sortedItems = useMemo(() => {
if (!sortConfig.key) return currentItems;

return [...currentItems].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
}, [currentItems, sortConfig]);

if (loading) {
return <div className="loading">Loading data...</div>;
}

if (error) {
return <div className="error">Error: {error.message}</div>;
}

return (
<div className="data-table">
<table className="table">
<thead>
<tr>
{columns.map(column => (
<th
key={column.key}
onClick={() => handleSort(column.key)}
className={sortConfig.key === column.key ? sortConfig.direction : ''}
>
{column.label}
{sortConfig.key === column.key && (
<span>{sortConfig.direction === 'ascending' ? ' ↑' : ' ↓'}</span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{sortedItems.map(item => (
<tr key={item.id}>
{columns.map(column => (
<td key={`${item.id}-${column.key}`}>
{column.render ? column.render(item[column.key]) : item[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>

{totalPages > 1 && (
<div className="pagination">
<button
onClick={goToPrevPage}
disabled={currentPage === 1}
>
Previous
</button>

{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => goToPage(page)}
className={page === currentPage ? 'active' : ''}
>
{page}
</button>
))}

<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
)}
</div>
);
}

// 使用示例
function App() {
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{
key: 'status',
label: 'Status',
render: status => (
<span className={`status ${status.toLowerCase()}`}>
{status}
</span>
)
}
];

return (
<div className="app">
<h1>User Management</h1>
<DataTable
url="https://api.example.com/users"
columns={columns}
itemsPerPage={5}
/>
</div>
);
}

案例2:实现一个可复用的表单组件

import React, { useState, useReducer, useCallback, useEffect } from 'react';

const initialState = {
values: {},
errors: {},
touched: {},
isSubmitting: false,
isValid: false
};

function formReducer(state, action) {
switch (action.type) {
case 'update_field':
return {
...state,
values: {
...state.values,
[action.field]: action.value
}
};
case 'set_error':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'set_touched':
return {
...state,
touched: {
...state.touched,
[action.field]: true
}
};
case 'set_validity':
return {
...state,
isValid: action.isValid
};
case 'set_submitting':
return {
...state,
isSubmitting: action.value
};
case 'reset':
return initialState;
default:
return state;
}
}

function useForm(fields, validate) {
const [state, dispatch] = useReducer(formReducer, initialState);

useEffect(() => {
const errors = validate(state.values);
const isValid = Object.keys(errors).length === 0;

dispatch({ type: 'set_validity', isValid });
}, [state.values, validate]);

const handleChange = useCallback((e) => {
const { name, value, type, checked, files } = e.target;

const fieldValue = type === 'checkbox' ? checked :
type === 'file' ? files[0] : value;

dispatch({
type: 'update_field',
field: name,
value: fieldValue
});

// 触摸验证
if (state.touched[name]) {
const fieldError = validateField(name, fieldValue);
dispatch({
type: 'set_error',
field: name,
error: fieldError
});
}
}, [state.touched]);

const handleBlur = useCallback((e) => {
const { name, value } = e.target;

dispatch({
type: 'set_touched',
field: name
});

const error = validateField(name, value);
dispatch({
type: 'set_error',
field: name,
error
});
}, []);

const handleSubmit = useCallback(async (onSubmit) => {
const errors = validate(state.values);
Object.entries(errors).forEach(([field, error]) => {
dispatch({ type: 'set_error', field, error });
});

if (Object.keys(errors).length === 0) {
dispatch({ type: 'set_submitting', value: true });

try {
await onSubmit(state.values);
} catch (error) {
console.error('Form submission error:', error);
} finally {
dispatch({ type: 'set_submitting', value: false });
}
}
}, [state.values, validate]);

const resetForm = useCallback(() => {
dispatch({ type: 'reset' });
}, []);

const getFieldProps = useCallback((fieldName) => {
const field = fields.find(f => f.name === fieldName);

return {
name: fieldName,
value: state.values[fieldName] || '',
error: state.errors[fieldName],
touched: state.touched[fieldName],
onChange: handleChange,
onBlur: handleBlur,
...field.props
};
}, [fields, state, handleChange, handleBlur]);

return {
values: state.values,
errors: state.errors,
touched: state.touched,
isSubmitting: state.isSubmitting,
isValid: state.isValid,
handleChange,
handleBlur,
handleSubmit,
resetForm,
getFieldProps
};
}

function validateField(name, value, fields) {
const field = fields.find(f => f.name === name);
if (!field) return '';

if (field.required && !value) {
return field.requiredMessage || `${field.label} is required`;
}

if (field.minLength && value.length < field.minLength) {
return `${field.label} must be at least ${field.minLength} characters`;
}

if (field.maxLength && value.length > field.maxLength) {
return `${field.label} must be at most ${field.maxLength} characters`;
}

if (field.pattern && !field.pattern.test(value)) {
return field.patternMessage || `${field.label} is invalid`;
}

return '';
}

function validateForm(values, fields) {
const errors = {};

fields.forEach(field => {
const error = validateField(field.name, values[field.name], fields);
if (error) {
errors[field.name] = error;
}
});

return errors;
}

function Form({ fields, onSubmit, className }) {
const form = useForm(fields, (values) => validateForm(values, fields));

return (
<form
className={className}
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit(onSubmit);
}}
>
{fields.map(field => {
const props = form.getFieldProps(field.name);

return (
<div key={field.name} className={`form-group ${props.error ? 'has-error' : ''}`}>
<label htmlFor={field.name}>
{field.label}
{field.required && <span className="required">*</span>}
</label>

{field.type === 'select' ? (
<select {...props}>
<option value="">Select {field.label}</option>
{field.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : field.type === 'textarea' ? (
<textarea {...props} rows={field.rows || 3} />
) : field.type === 'checkbox' ? (
<div className="checkbox-group">
<input type="checkbox" {...props} />
<label>{field.label}</label>
</div>
) : field.type === 'radio' ? (
<div className="radio-group">
{field.options.map(option => (
<div key={option.value} className="radio-option">
<input
type="radio"
id={`${field.name}-${option.value}`}
name={field.name}
value={option.value}
checked={props.value === option.value}
onChange={props.onChange}
/>
<label htmlFor={`${field.name}-${option.value}`}>
{option.label}
</label>
</div>
))}
</div>
) : (
<input type={field.type} {...props} />
)}

{props.touched && props.error && (
<div className="error-message">{props.error}</div>
)}
</div>
);
})}

<div className="form-actions">
<button
type="submit"
disabled={form.isSubmitting || !form.isValid}
className={`submit-btn ${form.isSubmitting ? 'loading' : ''}`}
>
{form.isSubmitting ? 'Submitting...' : 'Submit'}
</button>

<button
type="button"
onClick={form.resetForm}
className="reset-btn"
>
Reset
</button>
</div>
</form>
);
}

// 使用示例
function UserRegistration() {
const fields = [
{
name: 'username',
label: 'Username',
type: 'text',
required: true,
minLength: 3,
maxLength: 20
},
{
name: 'email',
label: 'Email',
type: 'email',
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
{
name: 'password',
label: 'Password',
type: 'password',
required: true,
minLength: 8
},
{
name: 'confirmPassword',
label: 'Confirm Password',
type: 'password',
required: true
},
{
name: 'country',
label: 'Country',
type: 'select',
required: true,
options: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' },
{ value: 'au', label: 'Australia' }
]
},
{
name: 'newsletter',
label: 'Subscribe to newsletter',
type: 'checkbox'
},
{
name: 'gender',
label: 'Gender',
type: 'radio',
required: true,
options: [
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' },
{ value: 'other', label: 'Other' }
]
}
];

const handleSubmit = async (values) => {
console.log('Form submitted:', values);
// 实际的表单提交逻辑
alert('Registration successful!');
};

return (
<div className="registration-form">
<h2>User Registration</h2>
<Form
fields={fields}
onSubmit={handleSubmit}
className="registration-form"
/>
</div>
);
}

案例3:实现一个可复用的模态框组件

import React, { useState, useCallback, useEffect, useRef } from 'react';

function useModal(initialState = false) {
const [isOpen, setIsOpen] = useState(initialState);
const [isAnimating, setIsAnimating] = useState(false);

const open = useCallback(() => {
setIsOpen(true);
}, []);

const close = useCallback(() => {
setIsAnimating(true);
setTimeout(() => {
setIsOpen(false);
setIsAnimating(false);
}, 300); // 动画持续时间
}, []);

const toggle = useCallback(() => {
if (isOpen) {
close();
} else {
open();
}
}, [isOpen, open, close]);

useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape' && isOpen) {
close();
}
};

const handleClickOutside = (e) => {
if (isOpen && e.target === e.currentTarget) {
close();
}
};

if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.addEventListener('click', handleClickOutside);
}

return () => {
document.removeEventListener('keydown', handleEscape);
document.removeEventListener('click', handleClickOutside);
};
}, [isOpen, close]);

return {
isOpen,
isAnimating,
open,
close,
toggle
};
}

function Modal({
isOpen,
isAnimating,
onClose,
children,
title,
size = 'md',
className = ''
}) {
const modalRef = useRef(null);

const backdropClass = `
fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300
${isAnimating ? 'opacity-0' : 'opacity-100'}
`;

const modalClass = `
fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
transition-all duration-300 ease-in-out
${isAnimating ? 'opacity-0 scale-95' : 'opacity-100 scale-100'}
${size === 'sm' ? 'w-96' : size === 'lg' ? 'w-3/4 max-w-4xl' : 'w-full max-w-2xl'}
${className}
`;

return (
<>
{/* Backdrop */}
{isOpen && (
<div
className={backdropClass}
onClick={onClose}
/>
)}

{/* Modal */}
{isOpen && (
<div ref={modalRef} className={modalClass}>
<div className="bg-white rounded-lg shadow-xl">
{/* Header */}
{title && (
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h3 className="text-xl font-semibold text-gray-900">{title}</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 focus:outline-none"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}

{/* Content */}
<div className="p-6">
{children}
</div>

{/* Footer */}
<div className="flex justify-end p-6 border-t border-gray-200">
<button
onClick={onClose}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Close
</button>
</div>
</div>
</div>
)}
</>
);
}

function useConfirm({
title = 'Confirm Action',
message = 'Are you sure you want to continue?',
confirmText = 'Confirm',
cancelText = 'Cancel',
onConfirm,
onCancel
}) {
const [isOpen, setIsOpen] = useState(false);
const [resolve, setResolve] = useState(null);

const showModal = useCallback((options = {}) => {
setIsOpen(true);

return new Promise((resolve) => {
setResolve(() => resolve);
});
}, []);

const handleConfirm = useCallback(() => {
setIsOpen(false);
if (onConfirm) onConfirm();
resolve(true);
}, [onConfirm, resolve]);

const handleCancel = useCallback(() => {
setIsOpen(false);
if (onCancel) onCancel();
resolve(false);
}, [onCancel, resolve]);

const ConfirmModal = useCallback(() => (
<Modal
isOpen={isOpen}
onClose={handleCancel}
title={title}
>
<p className="text-gray-700">{message}</p>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={handleCancel}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
{cancelText}
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 text-white bg-red-600 rounded-md hover:bg-red-700"
>
{confirmText}
</button>
</div>
</Modal>
), [isOpen, title, message, confirmText, cancelText, handleConfirm, handleCancel]);

return {
ConfirmModal,
show: showModal
};
}

function useConfirmDialog() {
const { ConfirmModal, show } = useConfirm({
title: 'Confirm Action',
message: 'Are you sure you want to perform this action?',
confirmText: 'Yes',
cancelText: 'No'
});

return {
ConfirmModal,
confirm: (options = {}) => show(options)
};
}

// 使用示例
function App() {
const [users, setUsers] = useState([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]);

const { ConfirmModal, confirm } = useConfirmDialog();

const handleDelete = async (userId) => {
const confirmed = await confirm({
title: 'Delete User',
message: 'Are you sure you want to delete this user?',
confirmText: 'Delete',
onConfirm: () => {
setUsers(users.filter(user => user.id !== userId));
}
});

if (confirmed) {
console.log('User deleted successfully');
}
};

return (
<div className="app">
<h1>User Management</h1>

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

<ConfirmModal />
</div>
);
}

总结

React Hooks彻底改变了我们编写React组件的方式。通过掌握这些Hook,我们可以:

  1. 更简洁的代码:避免class组件的复杂性
  2. 更好的复用性:通过自定义Hook复用逻辑
  3. 更好的性能:合理使用useMemo和useCallback
  4. 更好的可读性:代码更直观、更易理解

核心要点回顾

  1. 核心Hooks:useState、useEffect、useContext、useReducer
  2. 性能优化Hooks:useMemo、useCallback、React.memo
  3. 自定义Hooks:复用组件逻辑,保持代码整洁
  4. 最佳实践:遵循规则,避免常见问题
  5. 实战应用:复用组件、表单处理、模态框等

学习建议

  1. 从基础开始:先掌握useState和useEffect
  2. 逐步进阶:学习useReducer、useCallback等高级Hook
  3. 实践为主:通过实际项目练习
  4. 关注性能:学会使用性能优化Hook

Hooks是现代React开发的必备技能,希望这份指南能帮助你更好地使用React Hooks构建高质量的React应用。


最后更新:2026年5月18日
分类:IT | 知识学习