React hooks
React Hooks是React v16.8正式引入的特性,旨在解决与状态有关的逻辑重用和共享等问题。
在React Hooks诞生前,随着业务的迭代,在组件的生命周期函数中,充斥着各种互不相关的逻辑。通常的解决办法是使用Render Props动态渲染所需的部分,或者使用高阶组件提供公共逻辑以解耦各组件间的逻辑关联。但是,无论是哪一种方法,都会造成组件数量增多、组件树结构修改或者组件嵌套层数过多的问题。在Hooks诞生后,它将原本分散在各个生命周期函数中处理同一业务的逻辑封装到了一起,使其更具移植性和可复用性。使用Hooks不仅使得在组件之间复用状态逻辑更加容易,也让复杂组件更易于阅读和理解;并且由于没有类组件的大量polyfill代码,仅需要函数组件就可运行,Hooks将用更少的代码实现同样的效果。
React提供了大量的Hooks函数支持,如提供组件状态支持的useState、提供副作用支持的useEffect,以及提供上下文支持的useContext等。
useState
在 class 中,我们通过在构造函数中设置 this.state
为 { count: 0 }
来初始化 count
state 为 0,
而在函数组件中,我们没有 this
,所以我们不能分配或读取 this.state
。我们直接在组件中调用 useState
Hook:
function Example() {
// 声明一个叫 “count” 的 state 变量
const [count, setCount] = useState(0);
return <div>{count}</div>
}
useState类似于React类组件中的state和setState,可维护和修改当前组件的状态。
useState是React自带的一个Hook函数,使用useState可声明内部状态变量。useState接收的参数为状态初始值或状态初始化方法,它返回一个数组。数组的第一项是当前状态值,每次渲染其状态值可能都会不同;第二项是可改变对应状态值的set函数,在useState初始化后该函数不会变化。
useState的类型为:
function useState<S>(initialState:S|(() => S )): [S,Dispatch <SetStateAction <S>>];
initialState仅在组件初始化时生效,后续的渲染将忽略initialState:
const [value, setValue] = useState("");
const [count, setCount] = useState(value);
如上例中的value,当初始值传入另一个状态并初始化后,另一个状态函数将不再依赖value的值。
在 class 中,我们需要调用 this.setState()
来更新 count
值,在函数中,我们已经有了 setCount
和 count
变量,所以我们不需要 this:
import {useState} from "react";
const Example = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => {setCount(count+1)}}>
点击更新count:{count}
</button>
</div>
)
}
类似于setState,单击按钮时调用setCount更新了状态值count。当调用setCount后,组件会重新渲染,count的值会得到更新。
当传入初始状态为函数时,其仅执行一次,类似于类组件中的构造函数:
useState返回的更新函数也可使用函数式更新:
setCount(preCount => preCount + 1)
如果新的state需要依赖先前的 state 计算得出,那么可以将回调函数当作参数传递给setState。该回调函数将接收先前的state,并将返回的值作为新的state进行更新。
我应该使用单个还是多个 state 变量?
之前使用类组件的同学,你或许会试图总是在一次 useState()
调用中传入一个包含了所有 state 的对象。如果你愿意的话你可以这么做。这里有一个跟踪鼠标移动的组件的例子。我们在本地 state 中记录它的位置和尺寸:
function Box() {
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
// ...
}
现在假设我们想要编写一些逻辑以便在用户移动鼠标时改变 left
和 top
。注意到我们是如何必须手动把这些字段合并到之前的 state 对象的
useEffect(() => {
function handleWindowMouseMove(e) {
// 展开 「...state」 以确保我们没有 「丢失」 width 和 height
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
}
// 注意:这是个简化版的实现
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
这是因为当我们更新一个 state 变量,我们会 替换 它的值。这和 class 中的 this.setState
不一样,后者会把更新后的字段 合并 入对象中。
如果你还怀念自动合并,你可以写一个自定义的 useLegacyState
Hook 来合并对象 state 的更新。然而,我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。
举个例子,我们可以把组件的 state 拆分为 position
和 size
两个对象,并永远以非合并的方式去替换 position
:
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
把独立的 state 变量拆分开还有另外的好处。这使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易,比如说:
function Box() {
const position = useWindowPosition(); const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() { const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
}
注意看我们是如何做到不改动代码就把对 position
这个 state 变量的 useState
调用和相关的 effect 移动到一个自定义 Hook 的。如果所有的 state 都存在同一个对象中,想要抽取出来就比较难了。
在使用React Hooks时,需要遵守以下准则及特性要求。
-
只在顶层使用Hooks。不要在循环、条件或嵌套函数中调用Hooks,确保总是在React函数组件的顶层调用它们。
-
不要在普通的JavaScript函数中调用Hooks。仅在React的函数组件中调用Hooks,以及在自定义Hook中调用其他Hooks。