HOOKS
一、概要
:::
Hook 可以帮助在组件中使用不同的 React 功能。你可以使用内置的 Hook 或使用自定义 Hook。本页列出了 React 中常用 Hook。
:::
二、关键点
useState
useRef
useMemo
useCallback
useEffect
useContext
二、内容
Hook 原则
-
只在最顶层使用Hook;
-
不要在循环,条件或嵌套函数中调用Hook;
-
只在组件函数和自定义hook中调用Hook;
常用 Hook
useState
:::
useState 是一个 React Hook,它允许你向组件添加一个 状态变量。
:::
const [state, setState] = useState(initialState)
在组件顶层调用 useState 来声明一个状态变量。
import {useState} from "react";
export default function MyComponent () {
const [count,setCount] = useState(0)
return <div>
状态值:{count}
<button onClick={() => {
setCount(origin => origin + 1)
}}>加一</button>
</div>
}
这里通过数组解构
的方式来取 useState 的返回值,返回一个数组,第一个值是当前状态值,第二个是修改当前状态值的方法。
:::
注意:初始值 initialState
是可以接受任意值,但是对函数有特殊处理,被当做初始化函数执行将执行返回的结果作为初始值存储到 state 中,初次渲染后该函数将被忽略
:::
state
: 当前状态变量
React 会将最新的状态变量渲染到页面上。
setState
改变状态变量的方法
setState 函数,可以接受的入参为某个具体值,也可以是一个函数。
如果接受一个具体值:setState(value)
,会将该 value
传递给 state,并且触发 React 的渲染。
如果接受一个函数:setState(origin => origin + 1)
,该origin => origin + 1
将被执行,将返回值传递给 state,这个函数的入参是当前状态值,返回值是下一个状态值。
常见问题:
- 为什么拿不到最新的状态值?
import {useState} from "react";
export default function MyComponent () {
const [count,setCount] = useState(0)
return <div>
状态值:{count}
<button onClick={() => {
setCount(origin => origin + 1)
console.log(count)
}}>加一</button>
</div>
}
这里如果在第一次点击加一
按钮后,期待打印的是 1
,但是这个时候打印的是0
,这是因为状态表象是一个快照,更新状态会请求另一个渲染,但不会影响你当前的 js 执行中的 count 值,所以如果你要获取最新值,你应该这样写:
import {useState} from "react";
export default function MyComponent () {
const [count,setCount] = useState(0)
return <div>
状态值:{count}
<button onClick={() => {
const nextValue = count + 1
setCount(nextValue)
console.log(nextValue)
}}>加一</button>
</div>
}
- 为什么修改了状态没有触发渲染
obj.x = 10; // 🚩 错误:直接修改现有的对象
setObj(obj); // 🚩 不会发生任何事情
React 使用 Object.is 进行比较
const a = {value: 1}
const b = a;
Object.is(a,b); // true
b.value = 2;
Object.is(a,b); //true
const b = {value: 2}
Object.is(a,b); // false
所以应该给 setObj 传递一个新的值
setObj({
...obj,
x: 10
})
useRef
:::
useRef 是一个可以帮助引用不需要渲染的值
:::
在组件顶层调用 useRef
以声明一个 ref
const ref = useRef(initialState)
参数:
initialValue
:ref 对象的current
属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。
返回值:
useRef
返回一个只有一个属性的对象:
current
:初始值为传递的initialValue
。之后可以将其设置为其他值。如果将 ref 对象作为一个 JSX 节点的ref
属性传递给 React,React 将为它设置current
属性。
在后续的渲染中,useRef
将返回同一个对象。
注意:
:::
改变 ref 不会触发重新渲染
:::
-
可以修改
ref.current
属性。与 state 不同,它是可变的。然而,如果它持有一个用于渲染的对象(例如 state 的一部分),那么就不应该修改这个对象。 -
改变
ref.current
属性时,React 不会重新渲染组件。React 不知道它何时会发生改变,因为 ref 是一个普通的 JavaScript 对象。 -
除了初始化外不要在渲染期间写入或者读取
ref.current
,否则会使组件行为变得不可预测。
使用:
- 接受一个值
import {useRef} from "react";
export default function MyComponent () {
const ref = useRef<number>(0)
return <div>
ref值:{ref.current}
</div>
}
- 接受一个 DOM
import {useRef} from "react";
export default function MyComponent () {
const divRef = useRef<HTMLDivElement>(null)
return <div ref={divRef}>
<button
onClick={() => {
console.log(divRef.current?.clientHeight);
}}
>获取div的高度</button>
</div>
}
:::
注意:自定义组件默认是没有 ref 属性的,需要通过 forwardRef 这个高阶组件来包裹
:::
- 避免重复
React 会保存 ref 初始值,并在后续的渲染中忽略它。
function Video() {
const playerRef = useRef(new VideoPlayer());
// ...
虽然 new VideoPlayer()
的结果只会在首次渲染时使用,但是依然在每次渲染时都在调用这个方法。如果是创建昂贵的对象,这可能是一种浪费。
为了解决这个问题,你可以像这样初始化 ref:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
useEffect
:::
用来做同步操作,简单点说,这个 hook 有两个机制,一个是主动触发,一个是被动触发,先看如何声明和参数信息
:::
useEffect(setup, dependencies?)
在组件的顶层调用 useEffect
来声明一个 Effect:
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
参数 :
-
setup
:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。 -
可选
dependencies
:setup
代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像[dep1, dep2, dep3]
这样内联编写。React 将使用Object.is
来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。如果你想了解更多,请参见 传递依赖数组、空数组和不传递依赖项之间的区别。
返回值 :
useEffect
返回 undefined
。
注意事项 :
useEffect
是一个 Hook,因此只能在 组件的顶层 或自己的 Hook 中调用它,而不能在循环或者条件内部调用。如果需要,抽离出一个新组件并将 state 移入其中。
基于上述的案例,先说主动触发,主动触发在 ChatRoom 组件第一次渲染时,无论 roomId 和 serverUrl 为何值,内部函数都会被执行,当 roomId 或者 serverUrl 发生变更时,内部函数也会被触发。
useEffect 中第二个依赖参数有三种情况,
传递依赖项数组:
如果指定了依赖项,则 Effect 在 初始渲染后以及依赖项变更的重新渲染后 运行。
传递空数组:
如果你的 Effect 确实没有使用任何响应式值,则它仅在 初始渲染后 运行。
不传递:
如果完全不传递依赖数组,则 Effect 会在组件的 每次单独渲染(和重新渲染)之后 运行。
useCallback
:::
useCallback
是一个允许你在多次渲染中缓存函数的 React Hook。主要用于优化
:::
接受两个参数:
useCallback(fn, dependencies)
参数:
-
fn
:想要缓存的函数。此函数可以接受任何参数并且返回任何值。React 将会在初次渲染而非调用时返回该函数。当进行下一次渲染时,如果dependencies
相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。 -
dependencies
:有关是否更新fn
的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像[dep1, dep2, dep3]
这样编写。
返回值:
在初次渲染时,useCallback
返回你已经传入的 fn
函数
在之后的渲染中, 如果依赖没有改变,useCallback
返回上一次渲染中缓存的 fn
函数;否则返回这一次渲染传入的 fn
。
为什么要用 useCallback?
假设你正在从 ProductPage
传递一个 handleSubmit
函数到 ShippingForm
组件中:
function ProductPage({ productId, referrer, theme }) {
// ...
const handleSubmit = () => {
// ...
}
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件,因此每当因 theme
更改时而 ProductPage
组件重新渲染时,ShippingForm
组件也会重新渲染。这对于不需要大量计算去重新渲染的组件来说影响很小。这里涉及一个知识点就是在 JavaScript 中,**function () {}**
或者 **() => {}**
总是会生成不同的函数,所以每次渲染后,对于ShippingForm
组件来说,他接受的 onSubmit 作为 props 传递的属性来说每次结果都是变化的,如果对于内部将 onSubmit 作为依赖的一些 hook 来说,会频繁触发,而你不希望这样,就可以使用 useCallback 包裹 handleSubmit
function ProductPage({ productId, referrer, theme }) {
// ...
const handleSubmit = useCallback(() => {
// ...
},[])
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
这样,使用 Object.is 来比较这个 onSubmit 来说,就不会判断为更改
:::
**useCallback**
只应作用于性能优化。如果代码在没有它的情况下无法运行,请找到根本问题并首先修复它,然后再使用 useCallback
。
:::
useMemo
:::
useMemo
是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。主要用于优化
:::
接受两个参数:
useMemo(calculateValue, dependencies)
参数 :
-
calculateValue
:要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。React 将会在首次渲染时调用该函数;在之后的渲染中,如果dependencies
没有发生变化,React 将直接返回相同值。否则,将会再次调用calculateValue
并返回最新结果,然后缓存该结果以便下次重复使用。 -
dependencies
:所有在calculateValue
函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。如果你在代码检查工具中 配置了 React,它将会确保每一个响应式数据都被正确地定义为依赖项。依赖项数组的长度必须是固定的并且必须写成[dep1, dep2, dep3]
这种形式。React 使用Object.is
将每个依赖项与其之前的值进行比较。
返回值 :
在初次渲染时,useMemo
返回不带参数调用 calculateValue
的结果。
在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue
,并返回最新结果。
:::
你应该仅仅把 **useMemo**
作为性能优化的手段。如果没有它,你的代码就不能正常工作,那么请先找到潜在的问题并修复它。然后再添加 useMemo
以提高性能。
:::
useContext
:::
用于读取和订阅组件中的上下文
:::
这个 hook 相对比较复杂,需要时间和经验来吃透,可以先看下官方文档,大致了解概念和使用方式
三方应用: https://react.docschina.org/reference/react/useContext
三、小练习
- 下面这段代码点击两次
count+
按钮打印的分别是什么内容?
function MyComponent () {
const [count,setCount] = useState(0);
const getValue = useCallback(() => {
console.log(count);
},[]);
return <div>
{count}
<button
onClick={() => {
setCount(origin => origin + 1);
getValue()
}}
>count+</button>
</div>
}
-
为什么我直接修改了 useState 返回值的第一个值,并没有触发渲染?
-
我将 useRef 的返回值作为 useEffect 的依赖项传递,当我修改 useRef 的值,会不会触发 useEffect 的被动执行?为什么?
-
useCallback 和 useMemo 的共同点和不同点有哪些?
-
父子组件,兄弟组件之间数据传递有哪些方式?
-
props 和 state 的关系是什么?