React Hook最佳实践(附规范化组件模板)

1,541 阅读10分钟

React的用法千千万,Hook是React16.8版本新增的特性,让开发者在使用函数的情况下也可以编写有状态组件。此篇文章的目的是先结合Demo梳理官方常用Hooks的功能特点以及适用场景,最后探讨研究出一个符合规范的函数式组件编写模板。以便开发者参考借鉴,形成良好的函数式组件编写习惯。

什么是Hook

函数式组件可以使用Hooks。Hook是一些可以让你在函数组件里“钩入”React state及生命周期等特性的函数。

useState

用来声明状态变量,解决React UI层数据更新问题。

  1. 用法:useState会返回一对值:当前状态和一个让你更新它的函数,可以通过数组解构的方式获得,你可以在事件处理函数中或其他一些地方访问这个状态和调用这个函数。

  2. 特点:
    初始state参数只有在第一次渲染时会被用到。
    初始值可以是任意类型的。
    使用更新函数更新时进行值替换而不是值合并。
    可以声明多个state变量。

  3. 函数式更新:setState(c => c+1)如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。每次 setState 内部的回调取到的 state 是最新值。

  4. 时间切片带来的问题及解决方案:React架构的时间切片策略导致修改值后无法即时获得最新快照,可通过自定义syncState Hook来触发更新获得最新值。

Demo示例:

import React, { useState } from 'react';
import useSyncCallback from './useSyncState';


const WithState = () => {
			// 基础用法
      const [count, setCount] = useState(0);
      const [count3, setCount3] = useState(0);
      const [count4, setCount4] = useState(0);
      console.log('useState');

			// 使用自定义Hook:syncState
      const syncCallback = () => {
            setCount4(count4 + 1);
            output();
        };
    
        const output = useSyncCallback(() => {
            console.log(count4);
        });
      return (<div>
      			// 使用函数式更新写法
            <p>count:{count} <button onClick={() => { setCount(c => c + 1); console.log(count);}}>count+1/函数式更新打印输出慢一步</button></p>
            <p>count:{count3} <button onClick={() => { setCount3(count3 + 1);console.log(count3);}}>count+1/由于时间切片,打印输出慢一步</button></p>
            <p>count:{count4} <button onClick={syncCallback}>count+1/使用syncCallback实时打印输出</button></p>
      </div>)
};

export default WithState;

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

const useSyncState = (callback) => {
  // 增加一个state触发切片更新 且 默认值为false
  const [proxyState, setProxyState] = useState({ current: false });

  // useCallback避免重复init
  const Func = useCallback(() => {
      // 调用时即更改标志为true
      setProxyState({ current: true });
  }, [proxyState]);

  useEffect(() => {
    // 检查Func是否被缓存
    console.log('Func change');
  }, [Func]);

  // 结束后更改标志为false
  useEffect(() => {
    if (proxyState.current === true) setProxyState({ current: false });
  }, [proxyState]);

   useEffect(() => {
    // 每次均执行
    proxyState.current && callback();
  });

  return Func;
};

export default useSyncState;

useEffect

用于在渲染后根据依赖添加“副作用”。

  1. 执行时机:useEffect执行的是“副作用”,是在渲染后执行某些操作。它在第一次渲染之后和每次更新(取决于第二个参数依赖项)之后都会执行。但会保证在任何新的渲染前执行且在开始新的更新前,React 总会先清除上一轮渲染的 effect。

  2. 建议写法:使用多个Effect实现关注点分离,多个useEffect副作用拆分为多个,互不影响。

  3. 依赖项使用:
    3.1 第二个参数为依赖项数组,只有该依赖变化时才执行,可以填写state属性(包括对象类型,由于state是替换,每次都是新的值,为了实现真正的对象属性监听,需要指明属性);也可以填写函数fn,但由于函数一般定义在组件内,一旦组件重新渲染时,函数就会不断重新init,所以,最好结合useCallback使用,避免函数重复init导致fn依赖项失去意义。
    3.2 如果数组中有多个元素,即使只有一个元素发生变化,React也会执行effect。
    3.3 尽可能全的使用第二个依赖项数组参数。如果没有正确地指定所有的依赖,可能会在effect中获得“旧值”,比如如果一个 effect 指定了 [] 作为第二个参数,但在内部读取了 someProp,它会一直「看到」 someProp 的初始值。解决办法是要么移除依赖数组,要么修正它。因此当effect内部表达式或者函数调用需要获取数据最新准确的值时,就必须将相依赖的变量全部补充到依赖数组中。或者把这个函数移动到 effect 内部定义,就可以清楚地看到它用到的值并添加到依赖数组中。

  4. 清除副作用:在React组件中有两种常见副作用操作:需要清除的和不需要清除的。个别订阅外部数据源的场景需要使用return函数清除副作用,以免造成内存泄漏,该return函数在页面销毁前执行。

  • 异步获取数据后的赋值(取消请求);

  • 使用setInterval或者setTimeout的(清除定时器);

  • 添加监听事件addEventListener的(清除监听)。

Demo示例:

import React, { useEffect, useState, useCallback } from 'react';
import {useHistory} from 'react-router-dom';
import '../base.css';

let timer;
const WithEffect = () => {
      const [count, setCount] = useState(0);
      const [countObj, setCountObj] = useState({count:0, test:0});
      const [countWithoutEffect, setCountWithoutEffect] = useState(0);
      const history = useHistory();
      console.log('useEffect');

      // state:count的effect
      useEffect(() => {
            console.log('count的effect');
      }, [count])

      // state:countObj的effect
      useEffect(() => {
            console.log('countObj的effect');
      }, [countObj.test])

      // fn:staticFn的effect
      const staticFn = useCallback(() => {
            console.log('staticFn');
       }, []);

      useEffect(() => {
            console.log('staticFn的effect');
       }, [staticFn]);

      // 定时器的effect
      useEffect(() => {
            console.log('setInterval的effect');
            return () => {
                  console.log('销毁');
                  clearInterval(timer);
            };
      }, []);

      const autoAdd = () => {
            timer = setInterval(() => {
                  console.log('setInterval');
            }, 1000);
      };

      return (
            <div>
                  <div><button onClick={() => { setCount(count + 1)}}>点击属性count+1</button><span>count:{ count}</span></div>
                  <div><button onClick={() => { setCountWithoutEffect(count + 1) }}>点击更新未添加依赖的属性</button><span>countWithoutEffect:{countWithoutEffect}</span></div>
                  <div><button onClick={() => { setCountObj({...countObj, count: countObj.count +1})}}>点击更新对象类型countObj+1</button><span>countObj:{ countObj.count}</span></div>
                  <button onClick={staticFn}>点击执行函数</button>
                  <button onClick={autoAdd}>开始定时器</button>
                  <button onClick={() => { history.push('/other');}}>跳转其他页面前清除定时器</button>
            </div>
      )
      
};

export default WithEffect;

useMemo,memo

解决函数/子组件的重复渲染问题,返回的是一个值/组件。

  1. memo:用来缓存组件,由于通常情况下父组件渲染子组件随之重新渲染,使用memo后对子组件的渲染依据props进行简单比对缓存,无改动时不会频繁渲染子组件。但props.children每次均会根据父组件的init重新init,导致props是变化的,就失去了缓存的效果。

  2. useMemo写法:同useEffect写法类似,可添加依赖项。

  3. useMemo渲染时机:有返回值的,而返回值可以直接参与渲染的,所以useMemo是在渲染期间完成的。

  4. useMemo场景作用:返回的是计算的结果值,用于缓存计算后的状态,只有在依赖数据发生变化后,才会重新计算结果,起到缓存的作用。类似于vue的计算属性。

Demo示例:

import React, { useState, useMemo } from 'react';
import MemoChild from './memoChild';
import '../base.css';

export default function WithMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    console.log('useMemo');

    // 计算属性
    const expensive = useMemo(() => {
        console.log('computed');
        let sum = 0;
        for (let i = 0; i <= count; i++) {
            sum += i;
        }
        return sum;
    }, [count]);
 
    return <div>
        <p>count:{count}-inputVal:{val}-count累和:{expensive}</p>
        <div>
            <button onClick={() => setCount(count + 1)}>count+1/改动时count累和才重新渲染</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
        <MemoChild value={val}/>
    </div>;
}

import React, {memo} from 'react';

const MemoChild = memo((props) => {
      console.log('MemoChild', props);
      return (<div>
            <p>我是静态内容子组件</p>
      </div>)
 });

export default MemoChild;

useCallback

解决函数/组件的重复渲染问题,返回的是一个函数,可传递给子组件作为回调函数使用。

  1. useCallback返回的是函数,主要用来缓存函数,可作为回调函数传递给子组件,因为函数式组件中的state的变化都会导致整个组件被重新init(即使一些函数没有必要被init),此时用useCallback就会将函数进行缓存,减少渲染时的性能损耗;只有在依赖数据发生变化后,才会重新计算结果,起到缓存的作用。

Demo示例:

import React, { useState, useEffect, useCallback } from 'react';
import CallbackChild from './callbackChild';
import '../base.css';

const WithCallback = () => {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);
    console.log('useCallback');

    const increaseCounter1 = useCallback(() => {
        console.log('increaseCounter1的useCallback');
        setCount1(count1 + 1)
    }, [count1]);
    const increaseCounter2 = useCallback(() => {
        console.log('increaseCounter2的useCallback');
        setCount2(count2 + 1)
    }, [count2]);

    useEffect(() => {
        console.log('count1 change');
    }, [count1]);

    useEffect(() => {
        console.log('count2 change');
    }, [count2]);

    useEffect(() => {
        console.log('increaseCounter1 change');
    }, [increaseCounter1]);

    useEffect(() => {
        console.log('increaseCounter2 change');
    }, [increaseCounter2]);

    return <div>
        {/* 两个相同的组件,更新其中一个时另一个并不会重新渲染 */}
        <CallbackChild value={count1} clickHandle={increaseCounter1}/>
        <CallbackChild value={count2} clickHandle={increaseCounter2} />
    </div>;
}

export default WithCallback;

import React, { memo} from 'react';

const CallbackChild = memo(({ value, clickHandle }) => {
      console.log('CallbackChild');
      return <div>
                  <div onClick={clickHandle}>
                  我是子组件(事件函数通过父组件传递)<span>点击+1</span>: {value}
                  </div>
      </div>
});

export default CallbackChild;

useRef,createRef,forwardRef,useImperativeHandle

解决组件间引用,函数组件没有实例,只可以引用DOM元素。

  1. useRef:创建ref, 每次都会返回相同的引用。useRef 返回一个可变的 ref 对象,可以反应变量实时的状态值而非某一时刻的快照,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

  2. createRef:创建ref, 每次渲染都会返回一个新的引用。

  3. forwardRef:refs转发。接收父组件的ref并向下传递,作为其第二个参数,返回一个组件。可以将父组件的ref绑定到子组件自身的节点上。

  4. useImperativeHandle:让你在使用 ref 时自定义暴露给父组件的实例值。应当与 forwardRef 一起使用。

Demo示例:

import React, { useRef, useState, createRef } from 'react';
import RefChild from '../useImperativeHandle/refChild';

const WithImperativeHandle = () => {
      const [count, setCount] = useState(0);
      const countRef = useRef(0);
      const countCreate = createRef(0);
      const cRef = useRef();
      // 加10
      const addParent = () => {
            cRef.current.add();
      };
      return (<div>
            {/* 仅使用state保存的是某一时刻的快照 */}
            <p>点击了{count}次<button onClick={() => { setCount(count + 1) }}>点击</button><button onClick={() => { setTimeout(() => { console.log(count) }, 3000) }}>延时确认当前count</button></p>
            {/* 使用ref保存的是最新的值 */}
            <p><button onClick={() => { countRef.current = countRef.current+1;console.log(countRef.current); }}>点击</button><button onClick={() => { setTimeout(() => { console.log(countRef.current) }, 3000) }}>延时确认当前countRef</button></p>
            <p><button onClick={() => { countCreate.current = countCreate.current + 1; console.log(countCreate.current); }}>点击</button><button onClick={() => { setTimeout(() => { console.log(countCreate.current) }, 3000) }}>延时确认当前countCreateRef</button></p>
            {/* 使用子组件暴漏的函数 */}
            <button onClick={addParent}>在父组件点击加1</button>
            {/* 子组件引用 */}
            <RefChild ref={ cRef}/>
      </div>);
 };

export default WithImperativeHandle;

import React, { memo, forwardRef, useImperativeHandle, useState} from 'react';

const imperativeHandleChild = memo(forwardRef((props, ref) => {
      // 接收ref新属性
      const [count, setCount] = useState(1);
      console.log('imperativeHandleChild');

      const add = () => {
            setCount(count+1);
      };

      useImperativeHandle(ref, () => ({
            add, 
      }));

      return (<div ref={ref}>
            <p>我是子组件(事件函数我自己定义,我通过useImperativeHandle也暴露给了父组件)<button onClick={ add}>在我这里点击+1</button>count:{ count}</p>
      </div>);
 }));

export default imperativeHandleChild;

useReducer

useState的替代方案,解决state逻辑较复杂且包含多个子值,或者下一个state依赖于之前的state等场景。

  1. 能给那些会触发深更新的组件做性能优化

Demo示例:

import React, { useReducer } from "react";
import '../base.css';

const myReducer = (state, action) => {  
  switch(action.type) {    
    case('countUp'):      
    return {        
      ...state,        
      count: state.count + 1      
    }    
    default:      
    return state  
  }
}
  
function App() {  
  const [state, dispatch] = useReducer(myReducer, { count: 0 })  
  return (    
    <div className="App">
      <p>count: {state.count}<button onClick={() => dispatch({ type: 'countUp' })}>点击+1</button></p>    
    </div>  
  );
}

export default App;

useContext

解决组件间传值

Demo示例:

import React, { useState, useContext, createContext } from 'react';
import '../base.css';

// 定义全局变量
const CountContext = createContext();

function Child1() {
    console.log('useContext child');
    // 子组件使用
    const count = useContext(CountContext);

    return (
        <div>子组件count:{count}</div>
    )
}

function Parent() {
    const [count,setCount]=useState(0)
    console.log('useContext');

    const handleClick = () => {
        setCount(count+1);
    }
   
    return (
        <div>
            <div>
                父组件count:{count}
                <button onClick={handleClick}>在父组件+1</button>
                {/* 父组件使用 */}
                <CountContext.Provider value={count}>
                    <Child1></Child1>
                </CountContext.Provider>
            </div>
        </div>
    )
}

export default Parent;

规范化组件模板

// 父组件
import React, {useState, useEffect, useRef, useMemo, useCallback, memo} from 'react';
import Child from './child';

const Parent = memo(() => {
      console.log('parent 渲染了');
      const [count, setCount] = useState(0);
      const [time, setTime] = useState({now:Date.now()});
      // 引用
      const cRef = useRef();
      // 计算属性
      const doubleCount = useMemo(() => count * 2, [count]);

      // 添加count依赖,只有count变化时才会执行
      useEffect(() => {
            console.log('count 变了啊');
            return () => {}
      }, [count]);

      // 添加time依赖,只有time变化时才会执行
      useEffect(() => {
            console.log('time 变了啊');
            return () => {}
      }, [time.now]);

      // 函数缓存再传递给child组件,parent组件更新不会触发子组件更新
      const add = useCallback(() => {
            setCount(count => count + 1);
      }, []);

      return (<div>
            <p>我是父组件 count:{count}, double count: {doubleCount} <button onClick={add}>+1</button><button onClick={() => { cRef.current.fn() }}>点击调用子组件方法看看</button></p>
            <p>现在是北京时间 time:{time.now} <button onClick={() => { setTime({now:Date.now()});}}>更新时间</button></p>
            <Child ref={cRef} param="我是子组件点击看看" add={ add }/>
      </div>);
 });

export default Parent;

// 子组件
import React, { memo, forwardRef, useImperativeHandle, useCallback, useEffect } from 'react';

const Child = memo(forwardRef((props, ref) => {
      console.log('我使用了memo缓存组件,只有props有属性变化时我才会再次渲染');
      // 解构props
      const { param, add } = props;

      // 函数缓存,避免自组见刷新时重复init
      const fn = useCallback(() => {
            // 业务逻辑
            alert('我是子组件的方法');
      }, []);

      useEffect(() => {
            console.log('我添加了fn依赖,确认其用了useCallback只init了一次');
      }, [fn]);
      
      // 需要暴露给父组件的方法
      useImperativeHandle(ref, () => ({
            fn,
      }));
      
      return (<div><button onClick={fn}>{param}</button><button onClick={ add }>点我也可以更新count</button></div>);
 }));

export default Child;

解释说明:

引用

参考链接:React中文文档

文章Demo地址:react-demo