const [count, setCount] = useState(0);
You clicked {this.state.count} times
在函数中,我们直接使用 count 读取:You clicked {count} times
在函数中,我们已经将 setCount 和 count 作为变量,因此我们不需要 this :
[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' }]);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
提示:
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调用的 effects 不会阻止浏览器更新屏幕。这让你的应用程序感觉更有响应性。大多数 effects 不需要同步发生。在一些不常见的情况下(比如测量布局),有一个单独的useLayoutEffect Hook
,其 API 与 useEffect 相同。
需要清理的 side effects:
有些 effects 需要清理。 例如,我们可能希望设置对某些外部数据源的订阅。 在这种情况下,清理是非常重要的,这样我们就不会引起内存泄漏!
在React类中,通常会在** componentDidMount 中设置订阅**,然后在 componentWillUnmount 中清理订阅。例如:对定时器的设置和清除
componentDidMount() {
this.timer = setTimeout(() => {
nextTick();
}, 2000);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
useEffect(() => {
const timer = setTimeout(() => {
nextTick();
}, 2000);
return () => {
clearTimeout(timer); // Clean up the subscription
};
});
这是 effect 的可选清除机制。每个 effect 都可能返回一个在它之后进行清理的函数。这让我们可以将添加和删除订阅的逻辑紧密地保持在一起。它们是相同 effect 的一部分。
使用多个 Effects 来分离关注点:
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
});
}
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 来优化性能
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 直到浏览器绘制完成后,所以做额外的工作不是问题。
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 。 相反,你可以:
通过遵循此规则,您可以确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。
构建自己的 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 (
-
{props.friend.name}
);
}
我们想在 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 (
-
{props.friend.name}
);
}
这种写法完全等价于原始的写法,自定义 Hooks 是一种惯例,它自然地遵循 Hooks 设计的约定,而不是 React 特性。
注意:
两个组件使用相同的 Hook 共享 state(状态) 吗? 不会。
自定义 Hooks 是一种重用 stateful(有状态) 逻辑 的机制(例如设置订阅和记住当前值),但是每次使用自定义 Hook 时,它内部的所有状态和效果都是完全隔离的。
每次对 Hook 的调用都会被隔离。因为我们直接调用 useFriendStatus ,从 React 的角度来看,我们的组件只调用
useState
和useEffect
。正如我们 之前 所学到的 的,我们可以在一个组件中多次调用 useState 和 useEffect ,它们将完全独立的。
由于 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 (
<>
>
);
}
我们将当前选择的 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 (
Todos
{
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
this.addTodo(trimmedText);
}
}}
/>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render( , 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 (
Todos
{
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
addTodo(trimmedText);
}
}}
/>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( , rootElement);
组件与逻辑分开的Hooks写法:
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
import useTodoState from "./useTodoState";
const App = () => {
const { todos, addTodo, deleteTodo } = useTodoState([]);
return (
Todos
{
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
addTodo(trimmedText);
}
}}
/>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render( , rootElement);
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);
}
};
};
import useInputState from "./useInputState";
const TodoForm = ({ saveTodo }) => {
const { value, reset, onChange } = useInputState("");
return (
);
};
export default TodoForm;
import { useState } from "react";
export default initialValue => {
const [value, setValue] = useState(initialValue);
return {
value,
onChange: event => {
setValue(event.target.value);
},
reset: () => setValue("")
};
};