拥抱函数式编程、数据流和同步思维!!

HOOK概览

HOOK是 React 16.8.0 及之后出现的
组件:无状态组件(函数组件)、类组件

类组件中的麻烦:

  1. this指向问题(箭头函数、bind解决,但繁琐)
  2. 繁琐的生命周期
  3. 其他问题(不是函数式编程)

HOOK专门用于增强函数组件的功能(HOOK不能在类组件中使用),使之理论上可以成为类组件的替代品
官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件。只是鼓励使用函数组件

Hook使用规则:

Hook就是javascript函数,它可以让你“钩入” React 的特性。但是使用它们,会有两个额外的规则,React提供了一个 linter 插件来强制执行这些规则:

  1. 只能在函数最外层调用Hook,不要在循环、条件判断、或者子函数中调用
  2. 只能在React的函数组件中调用Hook,不要在其他javascript函数中调用(也可以在自定义的hook中调用hook)

只要 **Hook 的调用顺序在多次渲染之间保持一致**,React 就能正确地将内部 state 和对应的 Hook 进行关联。

个人对useEffect、Hook等的核心理解

https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

**每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次特定的渲染中定义的props和state。**

**在组件内什么时候去读取props或者state是无关紧要的。因为它们不会改变。在单次渲染的范围内,props和state始终保持不变。(解构赋值的props使得这一点更明显。)**

当然,有时候你可能想在effect的回调函数里读取最新的值而不是捕获的值。最简单的实现方法是使用refs(ref回调函数);

需要注意的是当你想要从过去渲染中的函数里读取未来的props和state,你是在逆潮而动。虽然它并没有错(有时候可能也需要这样做),但它因为打破了默认范式会使代码显得不够“干净”。这是我们有意为之的,因为它能帮助突出哪些代码是脆弱的,是需要依赖时间次序的。在class中,如果发生这种情况就没那么显而易见了。

effect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值:

**现在只需要记住:如果你设置了依赖项,effect中用到的所有组件内的值都要包含在依赖中。这包括props,state,函数 — 组件内的任何东西。然后尽可能减少依赖项,最小化。**

移除依赖的技巧:

  1. setState的函数形式, setCount(c => c+1);表达意图(递增状态),而不是结果
  2. 当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。
    • 解耦来自Actions的更新:当你写类似setSomething(something => ...)这种代码的时候,也许就是考虑使用reducer的契机。reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。
  1. React会保证dispatch在组件的声明周期内保持不变(也就是在每次渲染中都是一样的)

你可能会疑惑:这怎么可能?在之前渲染中调用的reducer怎么“知道”新的props?答案是当你dispatch的时候,React只是记住了action - 它会在下一次渲染中再次调用reducer。在那个时候,新的props就可以被访问到,而且reducer调用也不是在effect里。
这就是为什么我倾向认为useReducer是Hooks的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。结果是,这可以帮助我移除不必需的依赖,避免不必要的effect调用。

相比于直接在effect里面读取状态,它dispatch了一个action来描述发生了什么。这使得我们的effect和step状态解耦。我们的effect不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由reducer去统一处理:

  1. const [state, dispatch] = useReducer(reducer, initialState);
  2. const { count, step } = state;
  3. useEffect(() => {
  4. const id = setInterval(() => {
  5. dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
  6. }, 1000);
  7. return () => clearInterval(id);
  8. }, [dispatch]);
  9. /*===========================================================================*/
  10. const initialState = {
  11. count: 0,
  12. step: 1,
  13. };
  14. function reducer(state, action) {
  15. const { count, step } = state;
  16. if (action.type === 'tick') {
  17. return { count: count + step, step };
  18. } else if (action.type === 'step') {
  19. return { count, step: action.step };
  20. } else {
  21. throw new Error();
  22. }
  23. }
  • 我们可以把reducer函数放到组件内去读取props,这种模式会使一些优化失效,所以你应该避免滥用它,不过如果你需要你完全可以在reducer里面访问props

如果某些函数仅在effect中调用,你可以把它们的定义移到effect中:这么做有什么好处呢?我们不再需要去费力的考虑这些“间接依赖”。我们的依赖数组也不再撒谎:在我们的effect中确实没有再使用组件范围内的任何东西。

useEffect的设计意图就是要强迫你关注数据流的改变,然后决定我们的effects该如何和它同步 - 而不是忽视它直到我们的用户遇到了bug

组件内多个effect使用了相同的函数

  1. 第一个, 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在effects中使用
  2. 可以把这个函数包装成 useCallback Hook
    • useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。

函数是数据流的一部分吗

在class组件中,函数属性本身并不是数据流的一部分。组件的方法中包含了可变的this变量导致我们不能确定无疑地认为它是不变的。因此,即使我们只需要一个函数,我们也必须把一堆数据传递下去仅仅是为了做“diff”。我们无法知道传入的this.props.fetchData 是否依赖状态,并且不知道它依赖的状态是否改变了。

使用useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。感谢周到的useCallback,属性比如props.fetchData的改变也会自动传递下去。

我想强调的是,到处使用useCallback是件挺笨拙的事。当我们需要将函数传递下去并且函数会在子组件的effect中被调用的时候,useCallback 是很好的技巧且非常有用。或者你想试图减少对子组件的记忆负担,也不妨一试。但总的来说Hooks本身能更好地避免传递回调函数。

自定义HOOK

自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

  1. import React, { useState, useEffect } from 'react';
  2. //自定义useFriendStatus HOOK
  3. function useFriendStatus(friendID) {
  4. const [isOnline, setIsOnline] = useState(null);
  5. function handleStatusChange(status) {
  6. setIsOnline(status.isOnline);
  7. }
  8. useEffect(() => {
  9. ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
  10. return () => {
  11. ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
  12. };
  13. });
  14. return isOnline;
  15. }

useState

  1. 返回值:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。
  2. React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。
  3. 不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。
  4. **即便函数式组件重新执行(即重新渲染),setState这个函数的引用是一直不变的,不过useState返回的数组是在变的**

useEffect

  • componentDidMount, componentDidUpdate, componentWillUnMount
  1. useEffect是在render之后生效的
  2. effect在render后,按照代码先后顺序执行
  3. effect在没有任何依赖的情况下,render后每次都按照顺序执行
  4. effect内部执行是异步的
  5. 依赖 [] 可以实现类似componentDidMount的作用,但最好忘记生命周期,只记副作用
  6. effect回调函数(就是第一个参数),是按照先后顺序同时执行的
  7. effect的回调函数返回一个匿名函数(执行时机:组件卸载时,执行下一个effect前),
    相当于componentUnMount的钩子函数,一般是remove eventListener,clear timeId等,主要是组件卸载后防止内存泄漏.

综上所述,useEffect就是监听每当依赖变化时,执行回调函数的存在函数组件中的钩子函数

useCallback

  1. const memorizedCallback = useCallback(
  2. () => {
  3. doSomething(a, b);
  4. },
  5. [a, b],
  6. );//执行完这条语句并不会执行里面的回调函数,而是返回这个回调函数

返回一个memorized回调函数, 记住, 是函数!!

第一版: 每次render,handle都是新的函数,且每次都能拿到最新的data
第二版: 用useCallback包裹handle,每次render, handle也是新的函数,且每次都能拿到最新的data, 和一版效果一样, 所以不建议这么用
第三版: useCallback假如第二个参数deps,handle会被memoized, 所以每次data都是第一次记忆时候的data(闭包)
第四版: useCallback依赖count的变化,每当useCallback 变化时,handle会被重新memoized
第五版: 每当count变化时,传入子组件的函数都是最新的,所以导致child的useEffect执行

总结:
useCallback将返回一个记忆的回调版本,仅在其中一个依赖项已更改时才更改
当将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染时,此方法很有用
使用回调函数作为参数传递,每次render函数都会变化,也会导致子组件rerender, useCallback可以优化rerender
疑问:如何优化子组件不必要的渲染

useMemo

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

返回一个 memoized 值,和useCallback一样,当依赖项发生变化,才会重新计算 memoized 的值.
useMemo和useCallback不同之处是:它允许你将 memoized 应用于任何值类型(不仅仅是函数)。

  1. useMemo会在render前执行, **useMemo中传入的第一个参数(函数),也会在useMemo执行时在其内部执行**
  2. 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
  3. useMemo用于返回memorize,防止每次render时大计算量带来的开销
  4. 使用useMemo优化需谨慎, 因为优化本身也带来了计算,大多数时候,你不需要考虑去优化不必要的重新渲染

useRef

  1. useRef返回一个ref对象(**可以看作一个容器,但即便组件重新渲染,这个容器的引用也不会改变,都是同一个ref,只是其内部属性可变,比如current**),和自建一个{current: …}对象的唯一区别是,useRef会在每次渲染时返回同一个ref对象,在整个组件的生命周期内(就是包括更新渲染)是唯一的。
  2. useRef可以保存任何可变的值。其类似于在class中使用实例字段的方式。

总结:

  1. **useRef可以存储那些不需要引起页面重新渲染的数据**
  2. 如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用一个ref来保存它,修改它,并从中读取。

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

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 **则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。**

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

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

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

useReducer

  1. const initialState = {count: 0};
  2. function reducer(state, action){
  3. switch(action.type){
  4. case 'increment': return {count: state.count + 1};
  5. }
  6. }
  7. function Counter(){
  8. const [state, dispatch] = useReducer(reducer, initialState);
  9. return (
  10. <div>
  11. Count: {state.count}
  12. <button onClick = {()=>dispatch({type: 'increment'})}>+</button>
  13. </div>
  14. );
  15. }
  1. reducer本质就是一个与UI无关的纯函数
  2. reducer就是一个只能通过action将state从一个过程转换成另一个过程的纯函数
  3. useReducer就是一种通过(state, action) => newState 的过程,和redux工作方式一样
  4. 数据流:dispatch(action) => reducer更新state => 返回更新后的state

官方推荐以下场景需要useReducer更佳:

  1. state逻辑较复杂且包含多个子值,可以集中处理
  2. 下一个state依赖于之前的state
  3. 想更稳定的构建自动化测试用例
  4. 想深层级修改子组件的一些状态,使用useReducer还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递dispatch而不是回调函数
  5. 如果你用应用程序比较大,希望UI和业务能够分开维护

注意 React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变 这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch

useContext

  • 跨组件共享数据的钩子函数:简单来说Context的作用就是对它所包含的组件树提供全局共享数据的一种技术
  1. const MyContext = React.createContext();
  2. const value = useContext(MyContext);
  3. // MyContext 为 context 对象(React.createContext 的返回值)
  4. // useContext 返回MyContext的返回值。

**当前的 context 值由上层组件中距离当前组件最近的<MyContext.Provider> 的 value prop 决定。**

  1. useContext的组件总会在上下文值变化时重新渲染,所以包裹的东西越多,层级越深,性能受到的影响越大
  2. 的值发生变化时候,包裹的组件无论是否订阅内容价值,所有组件都会从新渲染演示中child2不应该rerender,如何避免不必要的渲染? 使用React.memo优化
  3. const child2 = React.memo((props)=>{return <div>Child2</div>;});
  4. 默认情况下React.memo只会对复杂对象做浅层对比,如果想要控制对比过程,请将自定义的比较函数通过第二个参数传入

链接:https://juejin.cn/post/6844903869609148430

useContext版login:

  1. // 定义初始化值
  2. const initState = {
  3. name: '',
  4. pwd: '',
  5. isLoading: false,
  6. error: '',
  7. isLoggedIn: false,
  8. }
  9. // 定义state[业务]处理逻辑 reducer函数
  10. function loginReducer(state, action) {
  11. switch(action.type) {
  12. case 'login':
  13. return {
  14. ...state,
  15. isLoading: true,
  16. error: '',
  17. }
  18. case 'success':
  19. return {
  20. ...state,
  21. isLoggedIn: true,
  22. isLoading: false,
  23. }
  24. case 'error':
  25. return {
  26. ...state,
  27. error: action.payload.error,
  28. name: '',
  29. pwd: '',
  30. isLoading: false,
  31. }
  32. default:
  33. return state;
  34. }
  35. }
  36. // 定义 context函数
  37. const LoginContext = React.createContext();
  38. function LoginPage() {
  39. const [state, dispatch] = useReducer(loginReducer, initState);
  40. const { name, pwd, isLoading, error, isLoggedIn } = state;
  41. const login = (event) => {
  42. event.preventDefault();
  43. dispatch({ type: 'login' });
  44. login({ name, pwd })
  45. .then(() => {
  46. dispatch({ type: 'success' });
  47. })
  48. .catch((error) => {
  49. dispatch({
  50. type: 'error'
  51. payload: { error: error.message }
  52. });
  53. });
  54. }
  55. // 利用 context 共享dispatch
  56. return (
  57. <LoginContext.Provider value={dispatch}>
  58. <...>
  59. <LoginButton />
  60. </LoginContext.Provider>
  61. )
  62. }
  63. function LoginButton() {
  64. // 子组件中直接通过context拿到dispatch,出发reducer操作state
  65. const dispatch = useContext(LoginContext);
  66. const click = () => {
  67. if (error) {
  68. // 子组件可以直接 dispatch action
  69. dispatch({
  70. type: 'error'
  71. payload: { error: error.message }
  72. });
  73. }
  74. }
  75. }

可以看到在useReducer结合useContext,通过context把dispatch函数提供给组件树中的所有组件使用
,而不用通过props添加回调函数的方式一层层传递。

使用Context相比回调函数的优势:

  1. 对比回调函数的自定义命名,Context的Api更加明确,我们可以更清晰的知道哪些组件使用了dispatch、应用中的数据流动和变化。这也是React一直以来单向数据流的优势。
  2. 更好的性能:如果使用回调函数作为参数传递的话,因为每次render函数都会变化,也会导致子组件rerender。当然我们可以使用useCallback解决这个问题,但相比useCallbackReact官方更推荐使用useReducer,因为React会保证dispatch始终是不变的,不会引起consumer组件的rerender。

其他HOOK

useImperativeHandle

ref:需要传递的ref
createHandle: 需要暴露给父级的方法。
deps: 依赖

useImperativeHandle(ref, createHandle, [deps]);
useImperativeHandle在当前组件render后执行
useImperativeHandle应当于forwardRef一起使用

useDebugValue

不常用,只能在React Developer Tools中看到

useLayoutEffect

不常用,与useEffect相同,但是它会在所有的DOM变更之后同步调用(也就是说可能引起阻塞)effect

React FAQ

生命周期方法要如何对应到 Hook?

  1. constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。
  2. getDerivedStateFromProps:改为 在渲染时 安排一次更新。
  3. shouldComponentUpdate:详见 下方 React.memo.
  4. render:这是函数组件体本身。
  5. componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
  6. getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

有类似实例变量的东西吗?

有!useRef() Hook 不仅可以用于 DOM refs。「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

我可以只在更新时运行 effect 吗?

这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。(如果你发现自己经常在这么做,你可以为之创建一个自定义 Hook。)

有类似 forceUpdate 的东西吗?

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。

通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:

  1. const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
  2. function handleClick() {
  3. forceUpdate();
  4. }

可能的话尽量避免这种模式。

我该如何测量 DOM 节点?

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

  1. function MeasureExample() {
  2. const [height, setHeight] = useState(0);
  3. const measuredRef = useCallback(node => {
  4. if (node !== null) {
  5. setHeight(node.getBoundingClientRect().height);
  6. }
  7. }, []);
  8. return (
  9. <>
  10. <h1 ref={measuredRef}>Hello, world</h1>
  11. <h2>The above header is {Math.round(height)}px tall</h2>
  12. </>
  13. );
  14. }

在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

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

在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 h1 组件在整个重新渲染期间始终存在。如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver 或基于其构建的第三方 Hook。

如果你愿意,你可以 把这个逻辑抽取出来作为 一个可复用的 Hook:

  1. function MeasureExample() {
  2. const [rect, ref] = useClientRect();
  3. return (
  4. <>
  5. <h1 ref={ref}>Hello, world</h1>
  6. {rect !== null &&
  7. <h2>The above header is {Math.round(rect.height)}px tall</h2>
  8. }
  9. </>
  10. );
  11. }
  12. function useClientRect() {
  13. const [rect, setRect] = useState(null);
  14. const ref = useCallback(node => {
  15. if (node !== null) {
  16. setRect(node.getBoundingClientRect());
  17. }
  18. }, []);
  19. return [rect, ref];
  20. }

我该如何实现 shouldComponentUpdate

你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较:

const Button = React.memo((props) => {
// 你的组件
});
这不是一个 Hook 因为它的写法和 Hook 不同。React.memo 等效于 PureComponent,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)

React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。

如何惰性创建昂贵的对象?

如果依赖数组的值相同,useMemo 允许你 记住一次昂贵的计算。但是,这仅作为一种提示,并不 保证 计算不会重新运行。但有时候需要确保一个对象仅被创建一次。

第一个常见的使用场景是当创建初始 state 很昂贵时:

  1. function Table(props) {
  2. // ⚠️ createRows() 每次渲染都会被调用
  3. const [rows, setRows] = useState(createRows(props.count));
  4. // ...
  5. }

为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState:

  1. function Table(props) {
  2. // ✅ createRows() 只会被调用一次
  3. const [rows, setRows] = useState(() => createRows(props.count));
  4. // ...
  5. }

React 只会在首次渲染时调用这个函数。参见 useState API 参考。

你或许也会偶尔想要避免重新创建 useRef() 的初始值。举个例子,或许你想确保某些命令式的 class 实例只被创建一次:

  1. function Image(props) {
  2. // ⚠️ IntersectionObserver 在每次渲染都会被创建
  3. const ref = useRef(new IntersectionObserver(onIntersect));
  4. // ...
  5. }

useRef 不会 像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:

  1. function Image(props) {
  2. const ref = useRef(null);
  3. // ✅ IntersectionObserver 只会被惰性创建一次
  4. function getObserver() {
  5. if (ref.current === null) {
  6. ref.current = new IntersectionObserver(onIntersect);
  7. }
  8. return ref.current;
  9. }
  10. // 当你需要时,调用 getObserver()
  11. // ...
  12. }

这避免了我们在一个对象被首次真正需要之前就创建它。如果你使用 Flow 或 TypeScript,你还可以为了方便给 getObserver() 一个不可为 null 的类型。

Hook 会因为在渲染时创建函数而变慢吗?

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。

除此之外,可以认为 Hook 的设计在某些方面更加高效:

  1. Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
  2. 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。

  1. useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作:
  1. // 除非 `a` 或 `b` 改变,否则不会变
  2. const memoizedCallback = useCallback(() => {
  3. doSomething(a, b);
  4. }, [a, b]);
  1. useMemo Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。
  2. 最后,useReducer Hook 减少了对深层传递回调的依赖,正如下面解释的那样。

如何避免向下传递回调?

useReducer + useContext 共享的是dispatch

如何从 useCallback 读取一个经常变化的值?

在某些罕见场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好。如果你想要记住的函数是一个事件处理器并且在渲染期间没有被用到,你可以 把 ref 当做实例变量 来用,并手动把最后提交的值保存在它当中:

  1. function Form() {
  2. const [text, updateText] = useState('');
  3. const textRef = useRef();
  4. useEffect(() => {
  5. textRef.current = text; // 把它写入 ref
  6. });
  7. const handleSubmit = useCallback(() => {
  8. const currentText = textRef.current; // 从 ref 读取它
  9. alert(currentText);
  10. }, [textRef]); // 不要像 [text] 那样重新创建 handleSubmit
  11. return (
  12. <>
  13. <input value={text} onChange={e => updateText(e.target.value)} />
  14. <ExpensiveTree onSubmit={handleSubmit} />
  15. </>
  16. );
  17. }

这是一个比较麻烦的模式,但这表示如果你需要的话你可以用这条出路进行优化。如果你把它抽取成一个自定义 Hook 的话会更加好受些:
我淦!下面的代码没弄清楚,似懂非懂

  1. function Form() {
  2. const [text, updateText] = useState('');
  3. // 即便 `text` 变了也会被记住:
  4. const handleSubmit = useEventCallback(() => {
  5. alert(text);
  6. }, [text]);
  7. return (
  8. <>
  9. <input value={text} onChange={e => updateText(e.target.value)} />
  10. <ExpensiveTree onSubmit={handleSubmit} />
  11. </>
  12. );
  13. }
  14. function useEventCallback(fn, dependencies) {
  15. const ref = useRef(() => {
  16. throw new Error('Cannot call an event handler while rendering.');
  17. });
  18. useEffect(() => {
  19. ref.current = fn;
  20. }, [fn, ...dependencies]);
  21. return useCallback(() => {
  22. const fn = ref.current;
  23. return fn();
  24. }, [ref]);
  25. }

**无论如何,我们都 不推荐使用这种模式 ,只是为了文档的完整性而把它展示在这里。相反的,我们更倾向于 避免向下深入传递回调。**

底层原理

React 是如何把对 Hook 的调用和组件联系起来的?
React 保持对当前渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。

每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。