React Hooks

    技术2022-08-03  95

    前言

    今天我们来学习一下React在V16.8.0,发布的神器React Hooks。熟悉React的同学肯定知道我们在编写React应用会面临以下几个问题

    组件间状态难以复用复杂组件会变的非常难看和难以理解写class到吐

    这些问题在React Hooks出来之后将变的非常渺小,下面我们一起来学习下!

    什么示Hooks

    Hooks是一些可以让我们在函数组件里使用React state及生命周期等特性的函数。我们来看个栗子🌰

    Hooks写法

    import React, { useState } from 'react'; function Example() { // 声明一个叫 “count” 的 state 变量。 const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

    class写法

    class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } }

    我们观察上面代码和我们写Class组件对比,没有了this.state而多了useState。 useState就是一个Hooks。通过在函数组件调用它给来给组件内部添加state。React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。

    我们的state是可以声明多个变量的,Hooks同样也支持

    function ExampleWithManyStates() { // 声明多个 state 变量! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); // ... }

    我们在数组解构时,可以给state变量取不同的名字。

    使用 Hooks

    什么时候我们会用 Hooks?

    我们在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hooks。

    分析 useState执行过程 const [count, setCount] = useState(0);

    我们在调用useState时,声明了一个变量叫count,并赋值0给它。这与我们在Class中的this.state = { count: 0 }; 是一样的。 而useState给我们返回了count, setCount 对应的就是我们Class的this.state.count 和 this.setState。

    使用规则

    只能在函数最外层调用 Hooks。不要在循环、条件判断或者子函数中调用。只能在 React 的函数组件中调用 Hooks。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hooks —— 就是自定义的 Hooks 中,我们稍后会学习到。)

    监听数据

    那我们怎么监听数据变化呢?这里就要说一下useEffect了。 useEffect就是一个 Effect Hooks,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

    ps: 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让我们的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下,有单独的 useLayoutEffect Hooks 使用,其 API 与 useEffect 相同。

    看下面代码-_-

    import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // 相当于 componentDidMount 和 componentDidUpdate: useEffect(() => { // 使用浏览器的 API 更新页面标题 document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

    当调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行监听函数。由于监听函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。

    如何清除 Effect

    useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清除订阅 subscription.unsubscribe(); }; });

    非常简单,只需要在useEffect中返回一个函数即可。这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。React 会在组件卸载的时候执行清除操作。

    useEffect优化

    在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

    componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; } }

    而在useEffect 的 Hooks API 中, 只要传递数组作为 useEffect 的第二个可选参数即可:

    useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 仅在 count 更改时更新

    上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

    当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

    ps: 此方法同样对清除Effect有用

    自定义Hooks

    在前言中我们说到了使用Class组件的痛点。那我们来看一下怎么使用Hooks解决上面的问题。

    自定义 Hooks 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hooks。(必须使用use, 否则无法判定某个函数是否包含对其内部 Hooks 的调用)

    import { useState, useEffect } from 'react'; function useCountState(props) { const [count, setCount] = useState(0); setCount(count + 1) return {count, setCount}; }

    与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头。

    使用它

    function useCountCom(props) { const {count, setCount} = useCountState(props.count); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

    这里解释一下:

    在两个组件中使用相同的 Hooks 不会共享 state?我们直接调用了 useCountCom 从 React 的角度来看,我们的组件只是调用了 useState 和 useEffect。我们可以在一个组件中多次调用 useState 和 useEffect,它们是完全独立的。

    useReducer

    const [state, dispatch] = useReducer(reducer, initialArg, init);

    useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(与Redux类似)

    const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }

    有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法;

    惰性初始化

    需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。

    function init(initialCount) { return {count: initialCount}; } 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({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }

    这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利

    useCallback

    const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

    返回一个 memoized 回调函数。

    把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

    useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

    useMemo

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

    返回一个 memoized 值。

    把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

    记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

    如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

    可以把 useMemo 作为性能优化的手段,将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

    useRef

    const refContainer = useRef(initialValue);

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

    一个常见的用例便是命令式地访问子组件:

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

    本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

    ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

    然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

    这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

    请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

    useImperativeHandle

    useImperativeHandle(ref, createHandle, [deps])

    useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

    function FancyInput(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput);

    渲染 <FancyInput ref={inputRef} />的父组件可以调用 inputRef.current.focus()。

    useLayoutEffect

    其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

    尽可能使用标准的 useEffect 以避免阻塞视觉更新。

    如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect。

    如果你使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)。

    若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && <Child /> 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。

    useDebugValue

    useDebugValue(value)

    useDebugValue 可用于在 React 开发者工具中显示自定义 hooks 的标签。

    例如,自定义 Hooks:

    function useCountState(props) { const [count, setCount] = useState(0); // 在开发者工具中的这个 Hook 旁边显示标签 // e.g. "useCountCom: 0" useDebugValue(count); return {count, setCount}; }

    不推荐向每个自定义 Hooks 添加 debug 值。当它作为共享库的一部分时才最有价值。

    延迟格式化 debug 值

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

    因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hooks 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

    例如,一个返回 Date 值的自定义 Hooks 可以通过格式化函数来避免不必要的 toDateString 函数调用

    Processed: 0.019, SQL: 9