React Hooks概述
由来
- Hooks 允许您在不更改组件层次结构的情况下重用有状态逻辑。
- Hooks允许您根据相关内容(例如设置订阅或获取数据)将一个组件拆分为较小的函数,而不是基于生命周期方法强制拆分。您还可以选择使用 reducer 管理组件的本地state(状态),以使其更具可预测性。
- Hooks 允许您在没有类的情况下使用更多 React 的功能。
基本语法
const [count, setCount] = useState(0);- count 相当于 之前的 state.count
当我们想要在类中显示当前计数时,我们使用 this.state.count 读取:
在函数中,我们直接使用 count 读取:<p>You clicked {this.state.count} times</p><p>You clicked {count} times</p> - setCount 相当于 this.setState(count)
在类中,我们需要调用 this.setState() 来更新 count 状态::
在函数中,我们已经将 setCount 和 count 作为变量,因此我们不需要 this :<button onClick={() => this.setState({ count:this.state.count + 1 })}> Click me </button><button onClick={() => setCount(count + 1)}> Click me </button> - useState(0) 相当于 状态以 { count: 0 } 开始
- count 相当于 之前的 state.count
- 使用多个 state(状态) 变量
将 state(状态) 变量 声明为一对[something, setSomething]也很方便,因为如果我们想使用多个状态变量,它可以为 不同 的 state(状态) 变量赋予不同的名称:
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
- Effect Hook
可以将 useEffect Hook 视为 componentDidMount,componentDidUpdate 和 componentWillUnmount的组合。
useEffect(() => {
document.title = `You clicked ${count} times`;
});
提示:
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调用的 effects 不会阻止浏览器更新屏幕。这让你的应用程序感觉更有响应性。大多数 effects 不需要同步发生。在一些不常见的情况下(比如测量布局),有一个单独的useLayoutEffect Hook,其 API 与 useEffect 相同。
-
需要清理的 side effects:
有些 effects 需要清理。 例如,我们可能希望设置对某些外部数据源的订阅。 在这种情况下,清理是非常重要的,这样我们就不会引起内存泄漏!
在React类中,通常会在** componentDidMount 中设置订阅**,然后在 componentWillUnmount 中清理订阅。例如:对定时器的设置和清除- 在 classes(类) 中的例子:
componentDidMount() { this.timer = setTimeout(() => { nextTick(); }, 2000); } componentWillUnmount() { clearTimeout(this.timer); }- 使用Hooks的例子:
useEffect(() => { const timer = setTimeout(() => { nextTick(); }, 2000); return () => { clearTimeout(timer); // Clean up the subscription }; });这是 effect 的可选清除机制。每个 effect 都可能返回一个在它之后进行清理的函数。这让我们可以将添加和删除订阅的逻辑紧密地保持在一起。它们是相同 effect 的一部分。
-
使用多个 Effects 来分离关注点:
- 在 classes(类) 中,注意设置 document.title 的逻辑如何在 componentDidMount 和 componentDidUpdate 之间拆分。订阅逻辑还分布在 componentDidMount 和 componentWillUnmount 之间。componentDidMount 包含两个任务的代码。例如:
class FriendStatusWithCounter extends React.Component { constructor(props) { super(props); this.state = { count: 0, isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { document.title = `You clicked ${this.state.count} times`; ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }- Hooks 如何解决这个问题呢?就像你可以多次使用 State Hook 一样,你也可以使用多个 effects 。这样,我们将不相关的逻辑分成不同的 effects :
function FriendStatusWithCounter(props) { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); const [isOnline, setIsOnline] = useState(null); useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); function handleStatusChange(status) { setIsOnline(status.isOnline); } // ... } -
通过跳过 Effects 来优化性能
- 在某些情况下,在每次渲染后清理或应用 effect 可能会产生性能问题。在类组件中,我们可以通过在
componentDidUpdate中编写与prevProps或prevState的额外比较来解决这个问题:
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; } }- 这个要求很常见,它被内置到
useEffect Hook API中。如果在重新渲染之间没有更改某些值,则可以告诉React 跳过应用 effect 。为此,将数组作为可选的第二个参数传递给 useEffect :
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // Only re-run the effect if count changes在上面的例子中,我们传递 [count] 作为第二个参数。如果count不发生改变,则React 会跳过这个 effect ,这是我们的优化。当count发生改变时,就算数组中有多个项目,React 也将重新运行 effect ,即使其中只有一个不同。
这也适用于具有清理阶段的 effect :
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }, [props.friend.id]); // Only re-subscribe if props.friend.id changes将来, 第二个参数可能会通过构建时转换自动添加。
注意
如果使用此优化,请确保该数组包含外部作用域中随时间变化且 effect 使用的任何值。 否则,您的代码将引用先前渲染中的旧值。 我们还将在 Hooks API 参考中讨论其他的优化选项。
如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。 这告诉React你的 effect 不依赖于来自 props 或 state 的任何值,所以它永远不需要重新运行。这不作为特殊情况处理 - 它直接遵循输入数组的工作方式。虽然传递[]更接近熟悉的 componentDidMount 和componentWillUnmount 心智模型,但我们建议不要将它作为一种习惯,因为它经常会导致错误,如上所述 。 不要忘记 React 推迟运行 useEffect 直到浏览器绘制完成后,所以做额外的工作不是问题。 - 在某些情况下,在每次渲染后清理或应用 effect 可能会产生性能问题。在类组件中,我们可以通过在
Hooks的规则
Hooks 是 JavaScript 函数,但在使用它们时需要遵循两个规则。 我们提供了一个 linter 插件 来自动执行这些规则:
-
只在顶层调用Hook
不要在循环,条件或嵌套函数中调用 Hook 。相反,总是在 React 函数的顶层使用 Hooks。通过遵循此规则,您可以确保每次组件渲染时都以相同的顺序调用 Hook 。 这就是允许 React 在多个 useState 和 useEffect 调用之间能正确保留 Hook 状态的原因。
React 依赖于调用 Hooks 的顺序 。只要 Hook 调用的顺序在每次渲染之间是相同的,React 就可以将一些本地 state(状态) 与每次渲染相关联。例如:
// ------------ // 第一次渲染 // ------------ useState('Mary') // 1. 用'Mary'初始化名称状态变量 useEffect(persistForm) // 2. 添加一个 effect 用于持久化form useState('Poppins') // 3. 使用 'Poppins' 初始化 surname 状态变量 useEffect(updateTitle) // 4. 添加一个 effect 用于更新 title // ------------- // 第二次渲染 // ------------- useState('Mary') // 1. 读取 name 状态变量(忽略参数) useEffect(persistForm) // 2. 替换 effect 以持久化 form useState('Poppins') // 3. 读取 surname 状态变量(忽略参数) useEffect(updateTitle) // 4. 替换 effect 用于更新 title // ...但是如果我们在条件中放置 Hook 调用(例如,persistForm effect)会发生什么呢?
// 我们在条件语句中使用Hook,打破了第一条规则 if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); }name !== ‘’ 条件在第一次渲染时为 true ,因此我们运行此 Hook 。 但是,在下一次渲染时,用户可能会清除form,使条件置为 false 。 现在我们在渲染过程中跳过此 Hook ,Hook 调用的顺序变得不同:
useState('Mary') // 1. 读取 name 状态变量(忽略参数) // useEffect(persistForm) // 这个Hook被跳过了 useState('Poppins') // 2 (但是之前是 3). 读取 surname 状态变量失败 useEffect(updateTitle) // 3 (但是之前是 4). 替换 effect 失败React 不知道第二次 useState Hook 调用返回什么。React 期望这个组件中的第二个 Hook 调用对应于 persistForm effect,就像之前的渲染一样,但现在已经不存在了。从那时起,在我们跳过的那个 Hook 调用之后的每一个 Hook 调用也会移动一个,从而导致 bug。
这就是为什么我们需要在组件顶层调用 Hook 的原因。 如果我们想要有条件地运行一个效果,我们可以把这个条件 放置 在我们的 Hook 中:
useEffect(function persistForm() { // ? 我们不再违反第一条规则了 if (name !== '') { localStorage.setItem('formData', name); } });请注意,如果使用 我们提供的lint规则 的话,就不需要担心这个问题
-
只在 React Functions 调用 Hooks
不要在常规 JavaScript 函数中调用 Hook 。 相反,你可以:
- React 函数式组件中调用 Hooks 。
- 从自定义 Hooks 调用 Hooks (我们将在下一页中学习 自定义 Hooks )。
通过遵循此规则,您可以确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。
构建自己的hooks
自定义Hooks
构建自己的 Hooks 可以将组件逻辑提取到可重用的函数中,可以使组件和逻辑分离。
-
有一个
FriendStatus组件,用于在聊天应用程序中,显示一条消息,指示朋友是否在线:import { useState, useEffect } from 'react'; function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; } -
还有一个
FriendListItem 组件,用于在聊天应用程序的应用程序的联系人列表中,将在线用户的用户名显示为绿色。import { useState, useEffect } from 'react'; function FriendListItem(props) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); }我们想在 FriendStatus 和 FriendListItem 之间分享这个逻辑。传统上,在 React 中,我们有两种流行的方式来共享组件之间的状态逻辑:render props(渲染属性) 和 higher-order components(高阶组件)。
我们现在将看看 Hook 如何在不强制您向树中添加更多组件的情况下解决许多相同的问题。
-
提取自定义Hook
当我们想要在两个 JavaScript 函数之间共享逻辑时,我们会将共享逻辑提取到第三个函数。自定义 Hook 是一个 JavaScript 函数,其名称以
use开头,可以调用其他 Hook。 例如,下面的useFriendStatus是我们的第一个自定义 Hook ,目的是订阅一个朋友的状态 :import { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } -
这样我们可以分别在
FriendStatus组件和FriendListItem 组件中使用它:import useFriendStatus from "./useFriendStatus"; function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }import useFriendStatus from "./useFriendStatus"; function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); }这种写法完全等价于原始的写法,自定义 Hooks 是一种惯例,它自然地遵循 Hooks 设计的约定,而不是 React 特性。
注意:
两个组件使用相同的 Hook 共享 state(状态) 吗? 不会。
自定义 Hooks 是一种重用 stateful(有状态) 逻辑 的机制(例如设置订阅和记住当前值),但是每次使用自定义 Hook 时,它内部的所有状态和效果都是完全隔离的。
每次对 Hook 的调用都会被隔离。因为我们直接调用 useFriendStatus ,从 React 的角度来看,我们的组件只调用
useState和useEffect。正如我们 之前 所学到的 的,我们可以在一个组件中多次调用 useState 和 useEffect ,它们将完全独立的。
在 Hooks 之间传递信息
由于 Hooks 是函数,所以我们可以在它们之间传递信息。
假如我们现在有一个ChatRecipientPicker组件,这是一个聊天消息收件人选择器,显示当前选择的朋友是否在线:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
我们将当前选择的 friend ID 保存在 recipientID state(状态) 变量中,如果用户在 选择器中选择了不同的好友,则更新它。
因为useState Hook 调用提供给我们 recipientID state(状态)变量的最新值,所以我们可以将它作为参数传递给自定义的 useFriendStatus Hook。这让我们知道当前选择的朋友是否在线。如果我们选择一个不同的朋友并更新 recipientID state(状态)变量,我们的 useFriendStatus Hook 将取消订阅以前选择的朋友,并订阅新选择的朋友的状态。
代码对比
我们现在写一个Add todo功能,代码如下 :
原始写法:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
this.deleteTodo = this.deleteTodo.bind(this);
this.addTodo = this.addTodo.bind(this);
}
componentDidMount() {
console.log(`You have ${this.state.todos.length} todos`);
}
componentDidUpdate() {
console.log(`You have ${this.state.todos.length} todos`);
}
addTodo(todoText) {
this.setState({
todos: [...this.state.todos, todoText]
});
}
deleteTodo(todoIndex) {
const newTodos = this.state.todos.filter((_, index) => todoIndex !== index);
this.setState({
todos: newTodos
});
}
render() {
return (
<div className="App">
<Typography variant="display3">Todos</Typography>
<TodoForm
saveTodo={todoText => {
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
this.addTodo(trimmedText);
}
}}
/>
<TodoList todos={this.state.todos} deleteTodo={this.deleteTodo} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hooks写法:
import { useState, useEffect } from 'react';
const App = () => {
const [todos, setTodo] = useState([]);
useEffect(() => {
console.log(`You have ${todos.length} todos`);
});
const addTodo = (todoText) => {
setTodo([...todos, todoText]);
}
const deleteTodo = (todoIndex) => {
const newTodos = todos.filter((_, index) => todoIndex !== index);
setTodo(newTodos);
}
return (
<div className="App">
<Typography variant="display3">Todos</Typography>
<TodoForm
saveTodo={todoText => {
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
addTodo(trimmedText);
}
}}
/>
<TodoList todos={todos} deleteTodo={deleteTodo} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
组件与逻辑分开的Hooks写法:
- index.js
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
import useTodoState from "./useTodoState";
const App = () => {
const { todos, addTodo, deleteTodo } = useTodoState([]);
return (
<div className="App">
<Typography variant="display3">Todos</Typography>
<TodoForm
saveTodo={todoText => {
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
addTodo(trimmedText);
}
}}
/>
<TodoList todos={todos} deleteTodo={deleteTodo} />
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
- useTodoState.js
import { useState, useEffect } from "react";
export default initialValue => {
const [todos, setTodos] = useState(initialValue);
useEffect(() => {
// Update the document title using the browser API
console.log(`You have ${todos.length} todos`);
});
return {
todos,
addTodo: todoText => {
setTodos([...todos, todoText]);
},
deleteTodo: todoIndex => {
const newTodos = todos.filter((_, index) => index !== todoIndex);
setTodos(newTodos);
}
};
};
- TodoForm.js
import useInputState from "./useInputState";
const TodoForm = ({ saveTodo }) => {
const { value, reset, onChange } = useInputState("");
return (
<form
onSubmit={event => {
event.preventDefault();
saveTodo(value);
reset();
}}
>
<TextField
variant="outlined"
placeholder="Add todo"
margin="normal"
onChange={onChange}
value={value}
/>
</form>
);
};
export default TodoForm;
- useInputState.js
import { useState } from "react";
export default initialValue => {
const [value, setValue] = useState(initialValue);
return {
value,
onChange: event => {
setValue(event.target.value);
},
reset: () => setValue("")
};
};
Hooks API
- Basic Hooks
- useState
- useEffect
- useContext
- Additional Hooks
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeMethods
- useLayoutEffect

644

被折叠的 条评论
为什么被折叠?



