编辑
2023-01-29
前端
00
请注意,本文编写于 661 天前,最后修改于 660 天前,其中某些信息可能已经过时。

目录

react hooks
基础 Hook
useSate
useEffect
useContext
额外的 Hook
useReducer
useCallback
useCallback 中依赖 state
useMemo
useMemo 与 useCallback 的异同
何时应该使用 useMemo
useRef
useRef 和 jsx 标签的 ref 属性的区别
回调 ref
useImperativeHandle (使用命令句柄)
useLayoutEffect
useDebugValue
useDeferredValue
useDeferredValue 与防抖的区别
useTransition
useId
useId 原理

https://zh-hans.reactjs.org/docs/hooks-reference.html

react hooks

本文不涉及 library hooks

基础 Hook

  • useState
  • useEffect
  • useContext

额外的 Hook

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue
  • useDeferredValue
  • useTransition
  • useId

基础 Hook

useSate

js
// import import {useState} from 'react' /* * state: 状态变量名称 * setState: 修改该状态值的函数名称 * initState: 默认值 */ const [state, setState] = useState(initState) // 如果默认值需要复杂计算获得,可以传入一个函数,该函数只有初始渲染会被调用 const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; }); /* * 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。 * 该函数将接收先前的 state,并返回一个更新后的值。 */ // x: 最新的 state 值 setState(x=> x+1) // 更改 state 为 111 setState(111) // 原理为 setState(x=> 111)

setState 的执行

  1. setState 是一个任务队列,队列中的 state 值基于当前的 react 快照 所以执行的代码中的 state 在执行前就已经确定了
js
// 假设 state = 1 setState(state + 1) // 1+1 setState(state + 1) // 1+1 setState(state + 1) // 1+1 // setState(state + 1) // 1+1 setState(x=> x+1) // 2+1 setState(47) // 47
  1. setState(111) 的实质是 setState(x=> 111)
  2. setState 是分发到队列中的,所以与组件中的代码不是同步执行的
js
// 每次渲染都针对的是当前的快照,那么 state 正常是不变的 console.log(state) //1 setState(state + 2) console.log(state) //1
  1. 如果更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。

useEffect

通常 useEffect 进行改变 DOM、网络请求、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作

js
useEffect( () => { // 类似 componentDidMount、componentDidUpdate // 执行网络请求、dom 操作、设置定时器 const subscription = props.source.subscribe(); return () => { // 组件卸载前执行,类似 componentWillUnmount subscription.unsubscribe(); }; }, /* * effect 所依赖的值数组,effect 只会在依赖值变化时进行执行 * 不填写该参数时(不是指[]),effect 默认跟随组件更新指定 * 该参数为 [] 时, effec 内的 props 和 state 会保持初始值,不跟随组件更新 */ [props.source], );

注意

  1. 如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。

    意味组件每次更新,在上例中都会创建新的订阅 如果不希望其他因素引起的组件更新都触发重新执行 effect,则需要指定 依赖值,这样 effect 只会在依赖值变化时进行执行

  2. 传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。

    意味着操作不会阻塞浏览器对屏幕的更新 如果需要监听 dom 变更并同步执行更改,则应该使用 useLayoutEffect

useContext

有一些变量,需要在组件之间共享 state

  • 如果是父子组件,则可以直接通过 props 进行传递
  • 如果跨越的层数较多,通过 props 就会变得复杂冗余,则可以使用 useContext 来共享

一般常见使用使用场景

  • 主题等全局设置、配置修改
  • 用户信息
  • 多层 state 关联,例如 h1 ~ h5
js
// 父组件 const themes = { light: { foreground: "#000000", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#222222" } }; // 创建一个 context // 名称为 ThemeContext,默认值为 themes.light const ThemeContext = React.createContext(themes.light); function App() { return ( // 使用 provider 包裹子组件,则子组件的 <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); }
js
// 子组件 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } // 子组件的子组件 function ThemedButton() { // 可以跨过子组件拿到 context const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }

注意

  1. 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
  2. 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,可以 通过使用 memoized 来优化。

额外的 Hook

useReducer

useReducer 是 useState 的替代方案,主要用于

  • state 逻辑较复杂且包含多个子值
  • state 的变化处理与一些固定值相关,例如抽象的操作类型 add delete,或状态 isActive
  • 下一个 state 依赖于之前的 state 等情况

工作模式类似 redux。 它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

js
const initialState = {count: 0}; // 初始化函数 function init(initialCount) { return {count: initialCount}; } // 根据 dispatch 鞋带的 action 信息,处理 state // 函数返回值就是 state 的结果 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() { /* * state: 状态变量 * dispatch: 携带 action 的 dispatch 名称 * reducer: 处理该 dispatch 的函数 * initialState: 默认值,可以直接填入值 * initFn: 惰性初始化函数,可选 */ const [state, dispatch] = useReducer(reducer, initialState, initFn); return ( <> Count: {state.count} <button /* action 一般都是一个对象携带 type 属性可以有其他属性*/ onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button /* 发送一个 dispatch指定操作 action */ onClick={() => dispatch({type: 'decrement'})} >-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }

相关信息

useReducer 和 useState 变化处理相同,如果返回值与当前 state 相同,react 不会进行组件更新

useCallback

useCallback是用来优化性能的,

useCallback返回一个 memoized(持久化) 回调函数,不会随组件更新生成新的函数,只有在依赖项变化的时候才会更新(返回一个新的函数)。

js
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
js
// 参考: https://www.jianshu.com/p/be8fb469d507 // 用于记录 getData 调用次数 let count = 0; function App() { const [val, setVal] = useState(""); function getData() { setTimeout(() => { setVal("new data " + count); count++; }, 500); } return <Child val={val} getData={getData} />; } function Child({val, getData}) { useEffect(() => { getData(); }, [getData]); return <div>{val}</div>; }

这里的例子中

  1. App渲染Child,将val和getData传进去
  2. Child使用useEffect获取数据。因为对getData有依赖,于是将其加入依赖列表
  3. getData执行时,调用setVal,导致App重新渲染
  4. App重新渲染时生成新的getData方法,传给Child
  5. Child发现getData的引用变了,又会执行getData
  6. 3 -> 5 是一个死循环

如果把 getData 从依赖中去除,则 getData 就只能执行一次 而实际情况可能,getData 改变时,是需要重新获取数据的。 这时,可以使用 useCallback 把 getData 持久化 这样,父组件传给子组件的 getData 函数就是稳定的,只有特定条件才会更新

js
// 父组件中使用 useCallback 将函数持久化 const getData = useCallback(() => { setTimeout(() => { setVal("new data " + count); count++; }, 500); }, []);

useCallback 中依赖 state

如果在 getData 中还需要使用到 val(useState 中的值),那么就只能将 val 加入依赖,这样的话,getData 又会死循环创建新的函数

js
const getData = useCallback(() => { console.log(val); setTimeout(() => { setVal("new data " + count); count++; }, 500); }, [val]);

如果我们希望无论val怎么变,getData的引用都保持不变,同时又能取到val最新的值,可以通过自定义 hook 实现。 注意这里不能简单的把val从依赖列表中去掉,否则getData中的val永远都只会是初始值(闭包原理)。

js
// 这里实现解决的原理是: 使用useRef 将函数引用保留 // 这样就不用重新创建新函数,只是更换了里面的函数内容 function useRefCallback(fn, dependencies) { const ref = useRef(fn); // 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域 // 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 fn useEffect(() => { ref.current = fn; }, [fn, ...dependencies]); return useCallback(() => { const fn = ref.current; return fn(); // 这里返回的最终函数只受 ref 影响 }, [ref]); } // 使用自定义钩子 const getData = useRefCallback(() => { console.log(val); setTimeout(() => { setVal("new data " + count); count++; }, 500); }, [val]);

useMemo

useMemo 与 useCallback 的异同

共同作用: 1.只有 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用。

两者区别: 1.useMemo 计算结果是 return 回来的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态 2.useCallback 计算结果是 函数, 主要用于 缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

参考文章

简而言之,useMemo是用来缓存计算属性的。 计算属性其实是函数的返回值,或者说指那些以返回一个值为目标的函数。 直接在渲染的时候就执行,在DOM区域被当作属性值一样去使用,被称为计算属性。 而计算属性,最后一定会使用return返回一个值!

js
const Com = () => { const [params1,setParams1] = useState(0); const [params2,setParams2] = useState(0); //这种是需要我们手动去调用的函数 const handleFun1 = () => { console.log("我需要手动调用,你不点击我不执行"); setParams1(val => val +1); } //这种被称为计算属性,不需要手动调用,在渲染阶段就会执行的。 const computedFun2 = () => { console.log('我又执行计算了'); return params2; } return <div onClick = {handleFun1}> //每次重新渲染的时候我就会执行 computed:{computedFun2()} </div> }

每次组件重新渲染都会让我们去执行computedFun2函数(计算属性) 尽管computedFun2函数中只使用到了params2状态,与被改变的状态并没有任何关系。

如果computedFun2函数里面的计算过程非常的复杂,那么每次重新计算无疑的非常麻烦的。

useMemo 可以让组件,在改变与计算属性无关的状态的时候,进行的渲染不触发我们计算属性的重新计算

js
const Com = () => { const [params1,setParams1] = useState(0); const [params2,setParams2] = useState(0); //这种是需要我们手动去调用的函数 const handleFun1 = () => { console.log("我需要手动调用,你不点击我不执行"); setParams1(val => val +1); } //这种被称为计算属性,不需要手动调用,在渲染阶段就会执行的。 const computedFun2 = useMemo(() => { console.log('我又执行计算了'); return params2; },[params2]) return <div onClick = {handleFun1}> //现在,我被useMemo保护,只有在组件初始化和params2改变的时候会执行 computed:{computedFun2()} </div> }

注意

useMemo是不是用的越多越好

缓存,需要成本!

在组件进行渲染并且此组件内使用了useMemo之后,为了校验改组件内被useMemo保护的这个计算属性是否需要重新计算,它会先去useMemo的工作队列中找到这个函数,然后还需要去校验这个函数都依赖是否被更改。 这其中,寻找到需要校验的计算属性和进行校验这两个步骤都需要成本。

何时应该使用 useMemo

  1. 某一个计算属性真的需要大量的计算时候
js
const Com = () => { //这种就是完全没必要被useMemo缓存的,计算过程一共也就一个创建变量,一个加一,缓存它反而亏本 const computedFun1 = () => { let number = 0; number = numebr +1; return number; } //这个就需要缓存一下了,毕竟他每次计算的计算量还是蛮大的。 const computedFun2 = () => { let number = 0; for(let i=0;i<100000;++i){ number = number +i-(number-i*1.1); } return number; } return <div onClick = {handleFun1}> computed1:{computedFun1()} //这个计算量小,是不需要使用useMemo缓存的,缓存它反而亏本 computed2:{computedFun2()} //这个计算量大,需要缓存。 </div> }
  1. 子组件依赖父组件的某一个依赖计算属性,并且子组件使用了React.memo进行优化了的时候

React.memo()其实是通过校验Props中的数据的内存地址是否改变来决定组件是否重新渲染组件的一种技术。

子组件传入一个计算属性,当父组件的其他State(与子组件无关的state)改变的时候。那么,因为状态的改变,父组件需要重新渲染,那被React.memo保护的子组件是否会被重新构建?

js
import {useMemo,memo} from 'react'; /**父组件**/ const Parent = () => { const [parentState,setParentState] = useState(0); //父组件的state //需要传入子组件的函数 const toChildComputed = () => { console.log("需要传入子组件的计算属性"); return 1000; } return (<div> <Button onClick={() => setParentState(val => val+1)}> 点击我改变父组件中与Child组件无关的state </Button> //将父组件的函数传入子组件 <Child computedParams={toChildComputed()}></Child> <div>) } /**被memo保护的子组件**/ const Child = memo(() => { consolo.log("我被打印了就说明子组件重新构建了") return <div><div> })

在上面这个例子中,是会被重新构建的

React.memo检测的是props中数据的栈地址是否改变 这里如果传入的是一个函数,如果该函数没有被缓存的话,栈地址会变化,也就会引起子组件的重新渲染。 这种情况可以使用 usememo 来缓存子组件接受的 props 来解决。 不用 useCallback 来缓存是因为,这里传入的是函数的结果,缓存的函数生成的结果仍然是新值,是一个新的存储地址

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。 返回的 ref 对象在组件的整个生命周期内持续存在

jsx
function TextInputWithFocusButton() { // 创建一个 ref 对象 const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); }; return ( <> /* 关联 ref */ <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }

注意

  1. 当 ref 对象内容发生变化时,useRef 并不会通知你
  2. useRef变化不会主动使页面渲染
  3. 如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用 回调 ref 来实现(见下)。

useRef 和 jsx 标签的 ref 属性的区别

ref 属性只是 dom 上的一个属性,用于指向 dom,方便操作

useRef() 比 ref 属性更有用,不仅可以用于 DOM refs,它可以很方便地保存任何可变值

「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

js
// 可以直接使用 useRef 存储非 dom 元素 function Timer() { const intervalRef = useRef(); useEffect(() => { const id = setInterval(() => { // ... }); intervalRef.current = id; return () => { clearInterval(intervalRef.current); }; }); // 定时器一旦使用 useRef 固定,就非常方便操作了 function handleCancelClick() { clearInterval(intervalRef.current); } // ... }

回调 ref

ref 属性可以是一个回调函数,该函数的参数为 dom 本身,可通过回调对 dom 进行定制化操作

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref 每当 ref 被附加到一个另一个节点,React 就会调用 callback。

js
function MeasureExample() { const [height, setHeight] = useState(0); // 回调传入一个 dom 本身 const measuredRef = useCallback(node => { // 回调内容对 dom 进行操作 if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []); return ( <> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> ); }

在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们,也就不会触发更新。

使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 函数不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

useImperativeHandle (使用命令句柄)

useImperativeHandle的作用是将子组件的指定元素暴漏给父组件使用。 也就是父组件可以访问到子组件内部的特定元素。 在大多数情况下,应当避免使用这样的命令式代码。

useImperativeHandle 应当与 forwardRef 一起使用

js
// 父组件 import {useRef} from "react" import Son from "./son" export default function(){ const eleP = useRef() const getElement = () => { console.log(eleP.current.ele1.current) console.log(eleP.current.ele2.current) } return <div> <button onClick={()=>getElement()}>点击获取子组件元素</button> // 提供保存子组件提供的元素的变量 ref <Son ref={eleP}/></div> } // 点击后触发结果: //<h2></h2> //<h3></h3>
js
// 子组件 import {useRef,forwardRef,useImperativeHandle} from "react" // ref 由父组件提供 function Son (props, ref){ const ele1 = useRef() const ele2 = useRef() // 使用 useImperativeHandle,将返回值提供给父组件 /* * 参数 1:ref: 父组件提供的存储【子组件共享的元素】变量 * 参数 2:回调函数,返回一个内容存储在父组件提供的 ref 中 * 参数 3:依赖数组,当数组内数据变化,会重新触发回调函数 */ useImperativeHandle(ref,()=>{ return {ele1,ele2} },[]) return <div > <h2 ref={ele1}></h2> <h3 ref={ele2}></h3> </div> } // Son = forwardRef(Son) export default Son

useLayoutEffect

useLayoutEffect与useEffect内部的实现其实是一样的.

传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。

唯一不同的点在于 useEffect异步处理副作用,而useLayoutEffect同步处理副作用

useEffect:

  • 运行在浏览器绘制之后,异步执行的,所谓的异步就是被 React 使用 requestIdleCallback 封装的,只在浏览器空闲时候才会执行

useLayoutEffect:

  • 运行在 render 之前,react 处理更新(diff 调度等)之后,同步阻塞后面的流程,看到页面时,已经完成处理
js
.center { text-align: center; margin: 0; padding: 0; } .square { position: absolute; top: 50%; left: 0; width: 100px; height: 100px; background: red; border-radius: 50%; } const Demo = () => { useEffect(() => { const square = document.querySelector(".square"); square.style.transform = "translate(-50%, -50%)"; square.style.left = "50%"; square.style.top = "50%"; }, []); return ( <div className="center"> <div className="square"></div> </div> ) };

因为useEffect是异步执行的,因此当页面渲染完毕后,square元素会直接从左上角移动到中间位置,导致页面会闪动一下。 将useEffect替换为useLayoutEffect后,页面中的圆球就直接在中间展示而不会从左上角偏移过来

相关信息

  1. 因为 useLayoutEffect 是同步执行的,所以我们也要避免在 useLayoutEffect 执行需要大量计算的内容,如果需要大量计算,就会导致页面的卡顿,从而造成页面的阻塞。
  2. 如果你使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。

useDebugValue

useDebugValue 用于在 React 开发者工具(如果已安装,在浏览器控制台 React 选项查看)中显示 自定义 Hook 的标签

useDebugValue的优势在于,用useDebugValue输出的值,是和DevTools中的Hook状态一起动态显示的,不需要在DevTools和Console面板中切换查看Hook状态和console.log输出。

useDebugValue()有两个参数

  1. debug 的值
  2. 是个回调函数,是个函数内部的函数,对第一个参数进行加工,然后返回个新的加工后的变量。(在最终的显示效果上,替代第一个变量...)
js
// 语法示例 useDebugValue("调试信息", (val) => { // val 就是 "调试信息" 这几个字 return "调试信息的再加工结果" });
js
// import React, { useState, useDebugValue } from 'react'; function useDiy2(num) { useDebugValue('111'); useDebugValue('222', (val) => { return + Date.now() + val }); const [count, setCount] = useState(num); const setCountDiy = (who) => { // 不能放在普通函数里面 // 只能放在组件或者use函数里面 // useDebugValue('888') setCount(count + 2); } return [count, setCountDiy]; } function App() { const [count2, setCount2] = useDiy2(10); // 可以放在组件里面,不报错但没效果 useDebugValue('111') return ( <> {count2} <button // 这不是个use所以不能用useDebugValue onClick={() => setCount2("333")}> </button> </> ) } export default App;

image.png

注意

在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。

useDeferredValue

参考内容

useDefferedValue 可以将值触发的计算优先级降低

如果说某些渲染比较消耗性能,比如存在实时计算和反馈,我们可以使用这个Hook降低其计算的优先级,使得避免整个应用变得卡顿。

js
// value: 默认值 // 延迟允许最长时间,到达该时间一定开始触发运算 const deferredValue = useDeferredValue(value, {timeoutMs: 1000}); <input value={text} onChange={handleChange}/> <List text={deferredText}/>
js
import React, {useState, useEffect} from 'react'; const List = (props) => { const [list, setList] = useState([]); const [count, setCount] = useState(0); useEffect(() => { setCount(count => count + 1); setTimeout(() => { setList([ {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, ]); }, 500); }, [props.text]); return [<p>{'我被触发了' + count + '次'}</p> , <ul>{list.map(item => <li>Hello:{item.name} value:{item.value}</li>)}</ul>] }; export default function App() { const [text, setText] = useState("哈哈哈"); const handleChange = (e) => { setText(e.target.value); }; return ( <div className="App"> <input value={text} onChange={handleChange}/> <List text={text}/> </div> ); };

一般我们的代码是这样写的。输入框的值变化的时候,我们使用setTimeout来模拟下向后端请求数据,或者大量计算的耗时操作。这个时候只要输入框的内容发生变化就会触发useEffect,我们用count来进行计数。

image.png

这样,List的Text变量就是一个延迟更新的值

useDeferredValue 与防抖的区别

useDeferredValue:

  • React 将在其他工作完成(而不是等待任意时间)后立即进行更新,并且像 startTransition 一样,延迟值可以暂停,而不会触发现有内容的意外降级。
  • timeoutMs这个参数的含义是延迟的值允许延迟多久,事实上网络和设备允许的情况下,React会尝试使用更低的延迟。
  • 只要变化一定会触发更新

防抖:

  • 防抖是固定时长后,才会触发新的动作
  • 防抖时间过了之后,变化才会触发更新
js
import React, {useState, useEffect, useDeferredValue} from 'react'; import {useDebounce} from 'ahooks'; const List = (props) => { const [list, setList] = useState([]); const [count, setCount] = useState(0); useEffect(() => { setCount(count => count + 1); setTimeout(() => { setList([ {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, {name: props.text, value: Math.random()}, ]); }, 500); }, [props.text]); return [<p>{'我被触发了' + count + '次'}</p>, <ul>{list.map(item => <li>Hello:{item.name} value:{item.value}</li>)}</ul>] }; export default function App() { const [text, setText] = useState("哈哈哈"); // useDefferedvalue const deferredText = useDeferredValue(text, {timeoutMs: 1000}); // 防抖 const debouncedValue = useDebounce(text, { wait: 1000 }); const handleChange = (e) => { setText(e.target.value); }; return ( <div className="App"> <input value={text} onChange={handleChange}/> <List text={deferredText}/> <List text={debouncedValue}/> </div> ); };

useTransition

参考内容

useTransition 可以将一个函数的操作降级

js
// 案例 import {useState} from "react"; import styles from "./index.module.css"; const Home:React.FC = () => { const [val,setVal] = useState(''); const [arr,setArr] = useState<number[]>([]); //根据用户输入获取查询列表 const getList = (e:any) => { setVal(e.target.value) let arr = Array.from(new Array(10000),item=>Date.now()) setArr(arr); } return ( <div className={styles.box}> <input value={val} onChange={getList}/> { arr.map((item,key)=>( <div key={key}>{item}</div> )) } </div> ) } export default Home;

我们快速在表单输入了abcd,但是很明显出现了卡顿现象,大约2s后表单和列表数据都被渲染 image.png

由于我们使用了useState对表单和列表的数据进行了更新,二者触发批量更新机制但无优先级差异。

实际上为了保证用户体验感,我们需要保证输入框的输入数据永远处于最新显示,而列表的提示可以稍微滞后显示。

js
// 使用说明 /* * pending: 是否正在排队等待执行 * transition: startTransition 函数名称,使用该函数包裹的内容将被降级执行 */ import {useState,startTransition} from "react"; const [pending,transition] = useTransition();
js
import React,{useState,useTransition} from "react"; const Home:React.FC = () => { const [val,setVal] = useState(''); const [arr,setArr] = useState<number[]>([]); // 创建hook const [pending,transition] = useTransition() const getList = (e:any) => { setVal(e.target.value) let arr = Array.from(new Array(10000),item=>Date.now()) // 这里将大运算进行了降级排队执行 transition(()=>{ setArr(arr); }) } return ( <div className={styles.box}> <input value={val} onChange={getList}/> { /* 根据运算状态渲染 */ pending?<h2>loading....</h2>:( arr.map((item,key)=>( <div key={key}>{item}</div> )) ) } </div> ) } export default Home;

image.png

image.png

useId

参考内容 原理参考

useId 用来生成一个唯一的 id 值,其实质是组件的层级结构得来的

js
// App.tsx const id = Math.random(); export default function App() { return <div id={id}>Hello</div> }

如果应用是CSR(客户端渲染),id是稳定的,App组件没有问题。 但如果应用是SSR(服务端渲染),那么App.tsx会经历:

  1. React在服务端渲染,生成随机id(假设为0.1234),这一步叫dehydrate(脱水)

  2. <div id="0.12345">Hello</div>作为HTML传递给客户端,作为首屏内容

  3. React在客户端渲染,React 还需要对该组件重新激活,用于参与新的渲染更新等过程中,生成随机id(假设为0.6789),这一步叫hydrate(注水)

客户端、服务端生成的id不匹配!

使用 useId 来解决

js
function Checkbox() { // 生成唯一、稳定id const id = useId(); return ( <> <label htmlFor={id}>Do you like React?</label> <input type="checkbox" name="react" id={id} /> </> ); );

注意

  1. useId 的生成只和结构相关,所以每个组件生成的 useId 是唯一的。 一个组件内使用两次 useId 不会生成两个不同的 id 如果需要两个,可以在 useId() 的基础自行生成第二个值
js
function Checkbox() { // 生成唯一、稳定id const id_1 = useId(); // 再次使用 useId 仍是相同结果 const id_2 = id_1 + 1 return ( <> <label htmlFor={id_1}>Do you like React?</label> <input type="checkbox" name="react" id={id} /> </> ); );
  1. useId 生成一个包含: 的字符串 token。这有助于确保 token 是唯一的
  • 在 CSS 选择器或 querySelectorAll 等 API 中不受支持
  • 不要在 css 选择器的命名,或 html 的标识中使用

useId 原理

假设应用的组件树如下图:

image.png

不管BC谁先hydrate,他们的层级结构是不变的,所以层级本身就能作为服务端、客户端之间不变的标识。

比如 B 可以使用 2-1 作为id,C 使用 2-2 作为id

image.png

如果组件A、D使用了useId,B、C没有使用,那么只需要为A、D划定层级,这样就能减少需要表示层级。

本文作者:Silon汐冷

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!