Appearance
React Hooks 系统
本文介绍React Hooks的基本概念和使用方法,包括useState、useEffect、useContext等常用钩子,以及自定义Hook的创建。
什么是Hooks?
Hooks是React 16.8引入的特性,它允许你在不编写class的情况下使用state以及其他的React特性。Hooks是完全可选的,100%向后兼容,没有破坏性改动。
为什么使用Hooks?
- 无需重构即可使用状态:在函数组件中使用状态逻辑,而不必转换为类组件
- 复用状态逻辑:在不强制组件层次结构的情况下重用状态逻辑
- 组织相关代码:将相互关联的代码(如设置订阅或获取数据)组合在一起,而不是按生命周期方法拆分
- 使用函数而非类:避免类组件中的this问题,更容易理解
Hooks规则
- 只在最顶层使用Hooks:不要在循环、条件或嵌套函数中调用Hooks
- 只在React函数中调用Hooks:在React函数组件和自定义Hooks中调用Hooks,不要在普通JavaScript函数中调用
useState
useState是最基本的Hook,它让你在函数组件中添加本地状态。
基本用法
jsx
import React, { useState } from 'react';
function Counter() {
// 声明一个叫 "count" 的 state 变量,初始值为0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}使用多个状态变量
jsx
function UserForm() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))}
placeholder="Age"
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
);
}使用对象状态
jsx
function UserForm() {
const [user, setUser] = useState({
name: '',
age: 0,
email: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prevUser => ({
...prevUser,
[name]: name === 'age' ? Number(value) : value
}));
};
return (
<form>
<input
name="name"
value={user.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="age"
type="number"
value={user.age}
onChange={handleChange}
placeholder="Age"
/>
<input
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
</form>
);
}函数式更新
当新的状态依赖于之前的状态时,应该使用函数式更新:
jsx
function Counter() {
const [count, setCount] = useState(0);
// 不好的方式 - 可能导致问题
const handleIncrement = () => {
setCount(count + 1); // 使用当前的count值
setCount(count + 1); // 仍然使用相同的count值
};
// 好的方式 - 使用函数式更新
const handleIncrementCorrect = () => {
setCount(prevCount => prevCount + 1); // 使用前一个状态
setCount(prevCount => prevCount + 1); // 使用更新后的前一个状态
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrementCorrect}>Increment twice</button>
</div>
);
}惰性初始化
如果初始状态需要通过复杂计算获得,可以传递一个函数给useState:
jsx
function createInitialTodos() {
console.log('Creating initial todos - expensive calculation');
return Array.from({ length: 50 }, (_, i) => ({ id: i, text: `Item ${i}` }));
}
function TodoList() {
// 这个函数只在组件首次渲染时执行一次
const [todos, setTodos] = useState(() => createInitialTodos());
return (
<div>
{todos.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
);
}useEffect
useEffect允许你在函数组件中执行副作用操作,如数据获取、订阅、手动DOM操作等。
基本用法
jsx
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于 componentDidMount 和 componentDidUpdate
useEffect(() => {
// 更新文档标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}依赖数组
jsx
function Example() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 仅在count更改时运行
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log('Title effect ran');
}, [count]); // 仅在count改变时更新
// 仅在组件挂载时运行一次
useEffect(() => {
console.log('Component mounted');
// 清理函数将在组件卸载时运行
return () => {
console.log('Component will unmount');
};
}, []); // 空依赖数组表示只在挂载和卸载时执行
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Enter your name"
/>
</div>
);
}清理副作用
jsx
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// 设置订阅
const connection = createConnection(roomId);
connection.connect();
connection.on('message', (message) => {
setMessages(prev => [...prev, message]);
});
// 清理函数
return () => {
connection.disconnect();
};
}, [roomId]); // 当roomId变化时重新订阅
return (
<div>
<h1>Welcome to {roomId}!</h1>
<ul>
{messages.map(message => (
<li key={message.id}>{message.text}</li>
))}
</ul>
</div>
);
}数据获取示例
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 重置状态
setUser(null);
setLoading(true);
setError(null);
// 定义异步函数
const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// 调用异步函数
fetchUser();
// 可选的清理函数
return () => {
// 如果需要,可以在这里取消请求
};
}, [userId]); // 当userId变化时重新获取
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}useContext
useContext让你可以订阅React的Context,而不必使用Context.Consumer组件。
创建和使用Context
jsx
import React, { createContext, useContext, useState } from 'react';
// 创建Context
const ThemeContext = createContext();
// 提供Context的父组件
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
// 提供值给Context
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div className={`App ${theme}`}>
<Header />
<Main />
<Footer />
</div>
</ThemeContext.Provider>
);
}
// 使用Context的子组件
function Header() {
// 使用useContext获取Context值
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
</header>
);
}
function Main() {
const { theme } = useContext(ThemeContext);
return (
<main>
<p>Current theme: {theme}</p>
</main>
);
}
function Footer() {
const { theme } = useContext(ThemeContext);
return (
<footer>
<p>Footer in {theme} mode</p>
</footer>
);
}使用多个Context
jsx
const UserContext = createContext();
const ThemeContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'John' });
const [theme, setTheme] = useState('light');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
function Layout() {
return (
<div>
<Header />
<Content />
</div>
);
}
function Content() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
return (
<section className={theme}>
<h1>Welcome, {user.name}!</h1>
<p>Theme: {theme}</p>
</section>
);
}useReducer
useReducer是useState的替代方案,适用于复杂的状态逻辑。
基本用法
jsx
import React, { useReducer } from 'react';
// 定义reducer函数
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
case 'set':
return { count: action.payload };
default:
throw new Error(`Unsupported action type: ${action.type}`);
}
}
function Counter() {
// 使用useReducer,提供reducer函数和初始状态
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
Increment
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
Decrement
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>
Set to 10
</button>
</div>
);
}复杂状态管理
jsx
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
id: Date.now(),
text: action.payload,
completed: false
}];
case 'toggle':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'delete':
return state.filter(todo => todo.id !== action.payload);
case 'clear_completed':
return state.filter(todo => !todo.completed);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todosReducer, []);
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch({ type: 'add', payload: text });
setText('');
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
<span onClick={() => dispatch({ type: 'toggle', payload: todo.id })}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'delete', payload: todo.id })}>
Delete
</button>
</li>
))}
</ul>
{todos.some(todo => todo.completed) && (
<button onClick={() => dispatch({ type: 'clear_completed' })}>
Clear completed
</button>
)}
</div>
);
}使用初始化函数
jsx
function init(initialCount) {
return { count: initialCount };
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({ initialCount = 0 }) {
// 第三个参数是初始化函数
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
Reset
</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}useCallback
useCallback返回一个记忆化的回调函数,只有当依赖项变化时才会更新。
基本用法
jsx
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
// 没有使用useCallback - 每次渲染都会创建新函数
const incrementWithoutCallback = () => {
setCount(c => c + 1);
};
// 使用useCallback - 只有当依赖项变化时才会创建新函数
const incrementWithCallback = useCallback(() => {
setCount(c => c + 1);
}, []); // 空依赖数组意味着这个函数只会被创建一次
// 依赖于某个状态的回调
const incrementBy = useCallback((amount) => {
setCount(c => c + amount);
}, []); // 没有依赖于外部变量,所以空数组就足够了
return (
<div>
<p>Count: {count}</p>
<p>Other state: {otherState}</p>
<button onClick={incrementWithCallback}>Increment (with callback)</button>
<button onClick={incrementWithoutCallback}>Increment (without callback)</button>
<button onClick={() => incrementBy(5)}>Increment by 5</button>
<button onClick={() => setOtherState(o => o + 1)}>
Update other state
</button>
<ChildComponent onIncrement={incrementWithCallback} />
</div>
);
}
// 使用React.memo优化子组件,只有当props变化时才重新渲染
const ChildComponent = React.memo(({ onIncrement }) => {
console.log('ChildComponent rendered');
return (
<button onClick={onIncrement}>Increment from child</button>
);
});依赖项示例
jsx
function SearchResults() {
const [query, setQuery] = useState('');
const [page, setPage] = useState(1);
// 这个函数依赖于query和page,所以它们应该在依赖数组中
const fetchResults = useCallback(async () => {
const response = await fetch(
`https://api.example.com/search?q=${query}&page=${page}`
);
const data = await response.json();
return data;
}, [query, page]); // 依赖于query和page
// 使用fetchResults...
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<button onClick={() => setPage(p => p + 1)}>Next Page</button>
<ResultList fetchResults={fetchResults} />
</div>
);
}
// 使用React.memo优化,只有当fetchResults变化时才重新渲染
const ResultList = React.memo(({ fetchResults }) => {
const [results, setResults] = useState([]);
useEffect(() => {
const getResults = async () => {
const data = await fetchResults();
setResults(data);
};
getResults();
}, [fetchResults]); // 依赖于fetchResults
return (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
});useMemo
useMemo返回一个记忆化的值,只有当依赖项变化时才重新计算。
基本用法
jsx
import React, { useState, useMemo } from 'react';
function ExpensiveCalculation({ a, b }) {
// 使用useMemo缓存计算结果
const result = useMemo(() => {
console.log('Computing result...');
// 模拟昂贵的计算
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return a * b + sum;
}, [a, b]); // 只有当a或b变化时才重新计算
return <div>Result: {result}</div>;
}
function App() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
const [counter, setCounter] = useState(0);
return (
<div>
<div>
<input
type="number"
value={a}
onChange={e => setA(Number(e.target.value))}
/>
<input
type="number"
value={b}
onChange={e => setB(Number(e.target.value))}
/>
</div>
<ExpensiveCalculation a={a} b={b} />
<div>
<p>Counter: {counter}</p>
<button onClick={() => setCounter(c => c + 1)}>
Increment counter (doesn't affect calculation)
</button>
</div>
</div>
);
}避免不必要的重新渲染
jsx
function TodoList({ todos, filter }) {
// 使用useMemo缓存过滤后的列表
const filteredTodos = useMemo(() => {
console.log('Filtering todos...');
return todos.filter(todo => {
if (filter === 'all') return true;
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
}, [todos, filter]); // 只有当todos或filter变化时才重新计算
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
{todo.text} {todo.completed ? '(completed)' : ''}
</li>
))}
</ul>
);
}
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Learn Hooks', completed: false },
{ id: 3, text: 'Build something awesome', completed: false }
]);
const [filter, setFilter] = useState('all');
const [counter, setCounter] = useState(0);
return (
<div>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<TodoList todos={todos} filter={filter} />
<div>
<p>Counter: {counter}</p>
<button onClick={() => setCounter(c => c + 1)}>
Increment counter (doesn't affect todos)
</button>
</div>
</div>
);
}记忆化对象
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 记忆化对象,避免每次渲染创建新对象
const userStyles = useMemo(() => ({
backgroundColor: user?.isPremium ? 'gold' : 'silver',
padding: '10px',
borderRadius: '5px',
color: user?.isPremium ? 'black' : 'white'
}), [user?.isPremium]);
if (!user) return <div>Loading...</div>;
return (
<div style={userStyles}>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.isPremium ? 'Premium User' : 'Regular User'}</p>
</div>
);
}useRef
useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数。返回的对象在组件的整个生命周期内保持不变。
访问DOM元素
jsx
import React, { useRef, useEffect } from 'react';
function TextInputWithFocusButton() {
// 创建ref
const inputRef = useRef(null);
// 点击按钮时聚焦输入框
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</div>
);
}保存可变值
jsx
function Stopwatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
// 使用ref保存interval ID
const intervalRef = useRef(null);
// 使用ref保存上一次渲染时的时间,不会触发重新渲染
const previousTimeRef = useRef(0);
useEffect(() => {
// 保存上一次的时间
previousTimeRef.current = time;
});
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
} else if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// 清理函数
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isRunning]);
const handleStartStop = () => {
setIsRunning(!isRunning);
};
const handleReset = () => {
setTime(0);
setIsRunning(false);
};
return (
<div>
<p>Current time: {time} seconds</p>
<p>Previous time: {previousTimeRef.current} seconds</p>
<button onClick={handleStartStop}>
{isRunning ? 'Stop' : 'Start'}
</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}避免不必要的重新渲染
jsx
function Counter() {
const [count, setCount] = useState(0);
// 使用ref保存计数,不会触发重新渲染
const countRef = useRef(0);
const incrementWithState = () => {
setCount(count + 1); // 触发重新渲染
};
const incrementWithRef = () => {
countRef.current += 1; // 不触发重新渲染
console.log(`Current ref count: ${countRef.current}`);
};
console.log('Component rendered');
return (
<div>
<p>State count: {count}</p>
<p>Ref count: {countRef.current}</p>
<button onClick={incrementWithState}>Increment state</button>
<button onClick={incrementWithRef}>Increment ref</button>
</div>
);
}useLayoutEffect
useLayoutEffect与useEffect相同,但它会在所有DOM变更之后同步调用。可以使用它来读取DOM布局并同步触发重渲染。
基本用法
jsx
import React, { useState, useLayoutEffect, useEffect } from 'react';
function LayoutEffectExample() {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const divRef = useRef();
// 在DOM更新后同步运行
useLayoutEffect(() => {
// 这会在浏览器绘制之前同步执行
const { width, height } = divRef.current.getBoundingClientRect();
setWidth(width);
setHeight(height);
}, []); // 仅在挂载时运行
// 对比useEffect(会在浏览器绘制后异步执行)
useEffect(() => {
console.log('useEffect ran');
}, []);
return (
<div>
<div
ref={divRef}
style={{ width: '100px', height: '100px', background: 'red' }}
>
Target div
</div>
<p>Measured width: {width}px</p>
<p>Measured height: {height}px</p>
</div>
);
}防止闪烁
jsx
function TooltipPosition() {
const [tooltipHeight, setTooltipHeight] = useState(0);
const tooltipRef = useRef();
// 使用useLayoutEffect防止闪烁
useLayoutEffect(() => {
const height = tooltipRef.current.offsetHeight;
setTooltipHeight(height);
}, []);
const tooltipStyle = {
position: 'absolute',
top: `-${tooltipHeight}px`,
left: 0,
background: 'black',
color: 'white',
padding: '5px',
borderRadius: '3px'
};
return (
<div style={{ position: 'relative', marginTop: '50px' }}>
<div ref={tooltipRef} style={tooltipStyle}>
This is a tooltip
</div>
<button>Hover me</button>
</div>
);
}useImperativeHandle
useImperativeHandle自定义使用ref时暴露给父组件的实例值。
jsx
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
// 使用forwardRef获取父组件传递的ref
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
// 只暴露我们想要父组件访问的方法
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
// 自定义方法
setValue: (value) => {
inputRef.current.value = value;
}
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const fancyInputRef = useRef();
const focusInput = () => {
fancyInputRef.current.focus();
};
const setInputValue = () => {
fancyInputRef.current.setValue('Hello from parent!');
};
return (
<div>
<FancyInput ref={fancyInputRef} />
<button onClick={focusInput}>Focus Input</button>
<button onClick={setInputValue}>Set Value</button>
</div>
);
}useDebugValue
useDebugValue可用于在React DevTools中显示自定义hook的标签。
jsx
import React, { useState, useEffect, useDebugValue } from 'react';
// 自定义Hook
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// 在React DevTools中显示自定义标签
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
// 延迟格式化
function useUserStatus(userId) {
const [user, setUser] = useState(null);
// 使用函数形式可以避免不必要的格式化
useDebugValue(user, user => user ? `User: ${user.name}` : 'No user');
// 获取用户逻辑...
return user;
}
function StatusIndicator() {
const isOnline = useOnlineStatus();
return (
<div>
You are {isOnline ? 'online' : 'offline'}
</div>
);
}自定义Hooks
自定义Hooks是一种重用状态逻辑的机制,它不复用state本身,而是复用状态逻辑。
创建自定义Hook
jsx
import { useState, useEffect } from 'react';
// 自定义Hook:获取窗口尺寸
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组意味着这个effect只在挂载和卸载时运行
return windowSize;
}
// 使用自定义Hook
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
<p>Window width: {width}px</p>
<p>Window height: {height}px</p>
{width < 768 ? (
<p>You are on a mobile device</p>
) : (
<p>You are on a desktop</p>
)}
</div>
);
}带参数的自定义Hook
jsx
// 自定义Hook:获取API数据
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
setLoading(false);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
// 清理函数
return () => {
isMounted = false;
};
}, [url, JSON.stringify(options)]); // 依赖于url和options
return { data, loading, error };
}
// 使用自定义Hook
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(
`https://api.example.com/users/${userId}`
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}组合多个Hooks
jsx
// 自定义Hook:表单处理
function useForm(initialValues = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prevValues => ({
...prevValues,
[name]: value
}));
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prevTouched => ({
...prevTouched,
[name]: true
}));
};
const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
setIsSubmitting(true);
// 这里可以添加验证逻辑
onSubmit(values);
setIsSubmitting(false);
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
// 使用自定义Hook
function SignupForm() {
const {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
} = useForm({
username: '',
email: '',
password: ''
});
const submitForm = async (formValues) => {
try {
// 发送数据到服务器
await fetch('https://api.example.com/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formValues)
});
alert('Signup successful!');
reset();
} catch (error) {
alert(`Error: ${error.message}`);
}
};
return (
<form onSubmit={handleSubmit(submitForm)}>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
name="username"
type="text"
value={values.username}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.username && errors.username && (
<div className="error">{errors.username}</div>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && (
<div className="error">{errors.email}</div>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
type="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && (
<div className="error">{errors.password}</div>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Sign Up'}
</button>
</form>
);
}Hooks的最佳实践
1. 遵循Hooks规则
- 只在最顶层调用Hooks
- 只在React函数组件和自定义Hooks中调用Hooks
2. 使用ESLint插件
bash
npm install eslint-plugin-react-hooks --save-dev在ESLint配置中:
json
{
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}3. 正确管理依赖数组
jsx
// 不好的做法 - 缺少依赖项
function Example({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(id).then(setData);
}, []); // 缺少id依赖项
// ...
}
// 好的做法 - 包含所有依赖项
function Example({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(id).then(setData);
}, [id]); // 正确包含id依赖项
// ...
}4. 避免过度使用状态
jsx
// 不好的做法 - 过度使用状态
function UserForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [address, setAddress] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [zip, setZip] = useState('');
// 每个字段都需要单独的处理函数
const handleFirstNameChange = (e) => setFirstName(e.target.value);
const handleLastNameChange = (e) => setLastName(e.target.value);
// ... 更多处理函数
// ...
}
// 好的做法 - 使用单个状态对象
function UserForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
address: '',
city: '',
state: '',
zip: ''
});
// 单个通用处理函数
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
// ...
}5. 使用函数式更新
jsx
// 不好的做法 - 可能导致竞态条件
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// 如果多次调用,可能不会正确更新
setCount(count + 1);
};
// ...
}
// 好的做法 - 使用函数式更新
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// 总是基于最新的状态更新
setCount(prevCount => prevCount + 1);
};
// ...
}6. 提取复杂逻辑到自定义Hooks
jsx
// 不好的做法 - 组件中包含复杂逻辑
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const userData = await fetchUser(userId);
setUser(userData);
const userPosts = await fetchPosts(userId);
setPosts(userPosts);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [userId]);
// 渲染逻辑...
}
// 好的做法 - 提取到自定义Hook
function useUserData(userId) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const userData = await fetchUser(userId);
setUser(userData);
const userPosts = await fetchPosts(userId);
setPosts(userPosts);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [userId]);
return { user, posts, loading, error };
}
function UserDashboard({ userId }) {
const { user, posts, loading, error } = useUserData(userId);
// 简化的渲染逻辑...
}7. 使用useCallback和useMemo优化性能
jsx
// 不好的做法 - 每次渲染都创建新函数和计算值
function SearchResults({ query, threshold }) {
const [results, setResults] = useState([]);
// 每次渲染都会创建新函数
const fetchResults = async () => {
const data = await fetch(`/api/search?q=${query}`);
const json = await data.json();
setResults(json);
};
// 每次渲染都会重新计算
const filteredResults = results.filter(
item => item.score > threshold
);
// ...
}
// 好的做法 - 使用useCallback和useMemo
function SearchResults({ query, threshold }) {
const [results, setResults] = useState([]);
// 只有当query变化时才创建新函数
const fetchResults = useCallback(async () => {
const data = await fetch(`/api/search?q=${query}`);
const json = await data.json();
setResults(json);
}, [query]);
// 只有当results或threshold变化时才重新计算
const filteredResults = useMemo(() => {
return results.filter(item => item.score > threshold);
}, [results, threshold]);
// ...
}8. 使用useReducer管理复杂状态
jsx
// 不好的做法 - 使用多个useState管理相关状态
function ShoppingCart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const addItem = (item) => {
setItems([...items, item]);
setTotal(total + item.price);
};
const removeItem = (itemId) => {
const item = items.find(i => i.id === itemId);
setItems(items.filter(i => i.id !== itemId));
setTotal(total - item.price);
};
// 更多操作...
}
// 好的做法 - 使用useReducer管理相关状态
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
case 'REMOVE_ITEM':
const item = state.items.find(i => i.id === action.payload);
return {
...state,
items: state.items.filter(i => i.id !== action.payload),
total: state.total - item.price
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0,
loading: false,
error: null
});
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: itemId });
};
// 更多操作...
}