React Hook概览

236 阅读16分钟


1. 简介

1.1 Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hook 就是 JavaScript 函数,使用它们有两个额外的规则:只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用;只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用。

1.2 函数组件

const Example = (props) => {  
   // 你可以在这使用 Hook  
   return <div />;
}

或者这样

function Example(props) {  
  // 你可以在这使用 Hook  
  return <div />;
}

2. 为什么使用hook

2.1 在组件之间复用状态逻辑很难

组件的嵌套,使得组件间的状态管理变得复杂,增大了耦合性,难以维护。使用 Hook 可以从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。这使得在组件间共享 Hook 变得更便捷。

2.2 复杂组件变得难以理解

我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

2.3 渐进策略

Hook 和现有代码可以同时工作,可以渐进式地使用他们,对已有的class组件不用修改。

3. State Hook

3.1 基本写法

首先看下面例子,State Hook写法:

 1:  import React, { useState } from 'react'; 
 2: 
 3:  function Example() { 
 4:    const [count, setCount] = useState(0); //数组解构写法 
 5: 
 6:    return ( 
 7:      <div> 
 8:        <p>You clicked {count} times</p> 
 9:        <button onClick={() => setCount(count + 1)}>
10:         Click me
11:        </button>
12:      </div>
13:    );
14:  }

  • 第一行: 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。

  • 第四行: 在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount。

  • 第九行: 当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。

等价的 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>    
       );  
}}

3.2 使用多个 state 变量

使用多个 state 变量,只需要我们给不同的 state 变量取不同的名称:

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

State 变量可以存储对象和数组,因此,可以将相关数据分为一组。另外,不像 class 中的 this.setState,useState中更新 state 变量总是替换它而不是合并它。

4. Effect Hook

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

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

副作用函数还可以通过返回一个函数来指定如何“清除”副作用。

跟 useState 一样,你可以在组件中多次使用 useEffect ,通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。

import React, { useState, useEffect } from 'react';

function Example() {  
     const [count, setCount] = useState(0);  

     useEffect(() => {    
       document.title = `You clicked ${count} times`;  
     });  

     return (    
       <div>     
         <p>You clicked {count} times</p>      
         <button onClick={() => setCount(count + 1)}>        
           Click me      
         </button>    
      </div>  
    );
}

4.1 无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。

使用 class 的示例

class Example extends React.Component {  
    constructor(props) {    
      super(props);    
      this.state = {      
        count: 0    
      };  
    }  
    componentDidMount() {    
       document.title = `You clicked ${this.state.count} times`;  
    }  

    componentDidUpdate() {    
       document.title = `You clicked ${this.state.count} times`;  
    }  

    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 中,我们需要在两个生命周期函数中编写重复的代码。

使用 Hook 的示例

import React, { useState, useEffect } from 'react';

function Example() {  
     const [count, setCount] = useState(0);  
     useEffect(() => {    
       document.title = `You clicked ${count} times`;  
     });  

     return (    
       <div>     
          <p>You clicked {count} times</p>     
          <button onClick={() => setCount(count + 1)}>        
             Click me     
          </button>    
       </div>  
     );
}

useEffect 做了什么?通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect?将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

4.2 需要清除的 effect

上面,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的,例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在来比较一下如何用 Class 和 Hook 来实现。

使用 Class 的示例

在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

class FriendStatus extends React.Component {  
    constructor(props) {    
     super(props);   
     this.state = { 
        isOnline: null 
     };    
     this.handleStatusChange = this.handleStatusChange.bind(this); 
    }  
    componentDidMount() {    
      ChatAPI.subscribeToFriendStatus(     
       this.props.friend.id,      
       this.handleStatusChange    
      );  
    }  
    componentWillUnmount() {    
      ChatAPI.unsubscribeFromFriendStatus(      
       this.props.friend.id,      
       this.handleStatusChange    
      );  
    }  
    handleStatusChange(status) {    
       this.setState({      
          isOnline: status.isOnline    
       });  
    }  
    render() {    
       if (this.state.isOnline === null) {      
         return 'Loading...';    
       }    
       return this.state.isOnline ? 'Online' : 'Offline';  
 }}

你会注意到 componentDidMount 和 componentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

使用 Hook 的示例

由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行,不需要单独的 effect 来执行清除操作。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {  
    const [isOnline, setIsOnline] = useState(null);  

    useEffect(() => {    
       function handleStatusChange(status) {      
         setIsOnline(status.isOnline);    
       }    
       ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    
       return function cleanup() {      
         ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    
        };  
     });  

     if (isOnline === null) {    
       return 'Loading...';  
     }  

     return isOnline ? 'Online' : 'Offline';
}

为什么要在 effect 中返回一个函数?这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。React 会在组件卸载的时候执行清除操作。

4.3 使用多个 Effect 实现关注点分离

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。下述代码是将前述示例中的计数器和好友在线状态指示器逻辑组合在一起的组件:

class FriendStatusWithCounter extends React.Component {  
    constructor(props) {    
       super(props);    
       this.state = { count: 0, isOnline: null };    
       this.handleStatusChange = this.handleStatusChange.bind(this);  
    }  

    componentDidMount() {    
       document.title = `You clicked ${this.state.count} times`;    
       ChatAPI.subscribeToFriendStatus(      
         this.props.friend.id,      
         this.handleStatusChange    
       );  
    }  

    componentDidUpdate() {    
       document.title = `You clicked ${this.state.count} times`;  
    }  

    componentWillUnmount() {   
       ChatAPI.unsubscribeFromFriendStatus(      
          this.props.friend.id,      
          this.handleStatusChange    
       );  
     }  

     handleStatusChange(status) {    
       this.setState({      
        isOnline: status.isOnline    
       }); 
     }  
     // ...

可以发现设置 document.title 的逻辑是如何被分割到 componentDidMount 和 componentDidUpdate 中的,订阅逻辑又是如何被分割到 componentDidMount 和 componentWillUnmount 中的。而且 componentDidMount 中同时包含了两个不同功能的代码。

那么 Hook 如何解决这个问题呢?就像你可以使用多个 state 的 Hook 一样,你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:

function FriendStatusWithCounter(props) {  
  const [count, setCount] = useState(0);  
  useEffect(() => {    
     document.title = `You clicked ${count} times`;  
  });
  
  const [isOnline, setIsOnline] = useState(null);  
  useEffect(() => {    
     function handleStatusChange(status) {      
       setIsOnline(status.isOnline);    
     }   
 
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    
     return () => {      
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    
      }; 
  });  
  // ...
}

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

4.4 通过跳过 Effect 进行性能优化

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

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

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

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

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

但是有一个问题,如果将所有函数都写在 useEffect 内部,那么当别的元素也调用同样的方法时还是重复写一遍代码,这时候要用到useCallback。

5. useCallback Hook

useCallback,它将函数抽取到 useEffect 外部。我们先看 useCallback 的用法:

function Counter() {  
   const [count, setCount] = useState(0);  
   const getTitle = useCallback(() => {    
      document.title = `You clicked ${count} times`;  
   }, [count]); 
 
   useEffect(() => {    
      getTitle();  
   }, [getTitle]);  

   return <h1>{count}</h1>;
}

可以看到,useCallback 有第二个参数 - 依赖项,我们将 getTitle 函数的依赖项通过 useCallback 打包到 getTitle 函数中,那么 useEffect 就只需要依赖 getTitle 这个函数,就实现了对 count 的间接依赖。

5.1 对比 useCallback 和 componentDidUpdate

回忆一下 Class Component 的模式,我们是如何在函数参数变化时进行重新取数的:

class Parent extends Component {  
    state = {    
      count: 0,    
      step: 0  
    };  

    getTitle = () => {    
      document.title = `You clicked ${count + step} times`;  
    }; 
 
    render() {    
      return <Child getTitle={this.getTitle} count={count} step={step}/>;  
    }
}

class Child extends Component {  
     state = {    
       data: null  
     };  

     componentDidMount() {    
       this.props.getTitle();  
     }  

     componentDidUpdate(prevProps) {    
       if ( this.props.count !== prevProps.count || this.props.step !== prevProps.step ) {      
           this.props.getTitle();    
       }  
     }  

     render() {   
         // ...  
     }
}

上面例子中 props.count 和 props.step 被 props.getTitle 函数使用了,因此在 componentDidUpdate 时,判断这两个参数发生了变化就触发重新取数。

然而这种写法很容易造成bug,父级函数 getTitle 依赖了什么参数以及参数的增加减少都需要在子函数 componentDidUpdate 时比较参数是否一致,否则将不会重新渲染,这种方式维护成本巨大。

用useCallback 替换试试:

function Parent() {  
     const [count, setCount] = useState(0);  
     const [step, setStep] = useState(0);    
     const getTitle = useCallback(() => {    
        document.title = `You clicked ${count + step} times`;  
     }, [count,step]);  

     return(    
        <Child getTitle={getTitle} />  
     )
}

function Child(props) {  
     useEffect(() => {    
         props.getTitle()  
     }, [props.getTitle]) 
     
     return (    
         // ...  
     )
}

可以看出来,当 getTitle 的依赖变化后,子组件代码不需要做任何改变,只需要关心依赖了 getTitle 这个函数即可,至于这个函数依赖了什么,已经封装在 useCallback 后打包透传下来了。

不仅解决了维护性问题,而且对于只要参数变化,就重新执行某逻辑,是特别适合用 useEffect 做的,使用这种思维思考问题会让你的代码更 “智能”。

5.2 将函数抽到组件外部

以上面的 getTitle 函数为例,如果要抽到整个组件的外部,就不是利用 useCallback 做到了,而是利用自定义 Hooks 来做:

function useGetTitle(count, step) {  
    return useCallback(() => {       
       document.title = `You clicked ${count + step} times`;  
    }, [count, step]);
}

可以看到,我们将 useCallback 打包搬到了自定义 Hook useGetTitle 中,那么函数中只需要一行代码就能实现一样的效果了:

function Parent() {  
    const [count, setCount] = useState(0);  
    const [step, setStep] = useState(0);  
    const getTitle = useGetTitle(count, step); 
    
    // 封装了 useGetTitle  
    useEffect(() => {    
      getTitle();  
    }, [getTitle]);  

    return (    
      <div>      
        <button onClick={() => setCount(c => c + 1)}>setCount {count}</button>      
        <button onClick={() => setStep(s => s + 1)}>setStep {step}</button>    
      </div>  
    );
}

但是这种用法当count 和 step 频繁变化,每次变化就会导致 useGetTitle 中 useCallback 依赖的变化,进而导致重新生成函数。但是这种函数是没必要每次都重新生成的,反复生成函数会造成大量性能损耗。

我们可以用 useRef 结合自定义Hook优化useCallback频繁调用的问题。

6. useRef Hook

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 创造一个自定义 Hook 代替 useCallback,使其依赖的值变化时,回调不会重新执行,却能拿到最新的值!

如下:

function useEventCallback(fn, dependencies) {  
    const ref = useRef(null);  
    
    useEffect(() => {    
      ref.current = fn;  
    }, [fn, ...dependencies]);  

    return useCallback(() => {    
      const fn = ref.current;    
      return fn();  
    }, [ref]);
}

首先看这一段:

useEffect(() => {  
    ref.current = fn;
}, [fn, ...dependencies]);

当 fn 回调函数变化时, ref.current 重新指向最新的 fn 。另外,当依赖项 dependencies 变化时,也重新为 ref.current 赋值,此时 fn 内部的 dependencies 值是最新的,而下一段代码:

return useCallback(() => {  
    const fn = ref.current;  
    return fn();
}, [ref]);

仅执行一次(ref 引用不会改变),但是却始终拿到最新的fn。

7. useReducer Hook

7.1 简介

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

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

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

useReducer 返回的结构与 useState 很像,只是数组第二项是 dispatch。以下是用 reducer 重写 useState 一节的计数器示例:

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>    
        </>  
      );
}

注:React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。所以可以在使用useReducer时,把useEffect依赖项设为dispatch来避免频繁的渲染。

7.2 初始化 useReducer state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。

第一种:将初始 state 作为第二个参数传入 useReducer (最简单的方法)

 const [state, dispatch] = useReducer(    
   reducer,    
   {count: initialCount}   
);

第二种:惰性初始化

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

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

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>    
         </>  
       );
}

7.3 跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

8. useMemo Hook

该Hook的作用等价于Class Component 的 PureComponent 。

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

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

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

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

const Child = (props) => {  
    useEffect(() => {    
       props.fetchData()  
    }, [props.fetchData])  

    return useMemo(() => (    
     // ...  
    ), [props.fetchData])
}

可以看到,我们利用 useMemo 包裹渲染代码,这样即便函数 Child 因为 props 的变化重新执行了,只要渲染函数用到的 props.fetchData 没有变,就不会重新渲染。

这里发现了 useMemo 的第一个好处:更细粒度的优化渲染。所谓更细粒度的优化渲染,是指函数 Child 整体可能用到了 A、B 两个 props,而渲染仅用到了 B,那么使用 useMemo 方案时,A 的变化不会导致重渲染。

9. useContext Hook

const value = useContext(MyContext);

使用 Context 做批量透传,在 Function Component 中,可以使用 React.createContext 创建一个 Context:

const Store = createContext(null);

其中 null 是初始值,一般设为 null 也没关系。接下来还有两步,分别是在根节点使用 Store.Provider 注入,然后在子节点使用官方 Hook useContext 拿到注入的数据。

在根节点使用 Store.Provider 注入:

function Parent() {  
    const [count, setCount] = useState(0);  
    const [step, setStep] = useState(0);  
    
    return (    
       <Store.Provider value={{ setCount, setStep }}>      
         <Child />   
       </Store.Provider>  
    );
}

在子节点使用 useContext 拿到注入的数据(也就是拿到 Store.Provider 的 value):

const Child = memo((props) => {  
    const { setCount } = useContext(Store)  

    function onClick() {    
       setCount(count => count + 1)  
    }  

    return (    
       // ...  
    )
})

这样就不需要在每个函数间进行参数透传了,公共函数可以都放在 Context 里。但是当函数多了,Provider 的 value 会变得很臃肿,我们可以结合之前讲到的 useReducer 解决这个问题。

9.1 使用 useReducer 为 Context 传递内容瘦身

使用 useReducer,所有回调函数都通过调用 dispatch 完成,那么 Context 只要传递 dispatch 一个函数就好了:

const Store = createContext(null);

function Parent() {  
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });  
  return (    
      <Store.Provider value={dispatch}>      
         <Child />    
      </Store.Provider>  
  );
}

这下无论是根节点的 Provider,还是子元素调用都会简化:

const Child = useMemo((props) => {  
     const dispatch = useContext(Store)  
     function onClick() {    
        dispatch({      
          type: 'countInc'    
         })  
     }  
     return (    
       // ...  
     )
})

那么将 state 也通过 Provider 注入呢?

9.2 将 state 也放到 Context 中

稍稍改造下,将 state 也放到 Context 中,这下赋值与取值都非常方便了!

const Store = createContext(null);

function Parent() {  
   const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });  
   return (    
      <Store.Provider value={{ state, dispatch }}>      
        <Count />      
        <Step />    
      </Store.Provider>  
   );
}

对 Count Step 这两个子元素而言,我们这么实现这两个子元素:

9.3 useMemo 配合 useContext

使用 useContext 的组件,如果自身不使用 props,就可以完全使用 useMemo 做性能优化:

const Count = () => {  
     const { state, dispatch } = useContext(Store); 
     return useMemo(() => (      
        <button onClick={() => dispatch("incCount")}> incCount {state.count}</button>    
     ), [state.count, dispatch]);
};

const Step = () => {  
     const { state, dispatch } = useContext(Store);  
     return useMemo(() => (      
        <button onClick={() => dispatch("incStep")}>incStep {state.step}</button>   
     ), [state.step, dispatch]  );
}


         

参考文章:

https://juejin.cn/post/6844903854174109703#heading-19

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

                                      

                                                                   --END--


未完待续......

最后,祝愿大家身体健康,常洗手,多通风。

欢迎关注GitHub:github.com/wlzhangYes

一点一滴积累,一步一步前进。分享工作中遇到的问题和日常琐事,欢迎关注公众号:南山zwl。


                          公众号                                                                      小程序