react hooks 入了个门

249 阅读3分钟

背景

由于新同学对 React hook 不熟悉,需要老同学的帮助。因此整理分享下hook相关的入门经验,希望对新同学有所帮助。

开始之前: 新同学首先要了解下官方文档

官网将 hook 区分为基础 hook额外的 hook。下面基于个人开发经验对hook做了更细致的分类。

hooks 简介

  • 状态管理

    • useState 管理组件状态
    • useReducer 同 useState ,但是可以定制更新状态的逻辑
    • useContext 跨组件管理状态
  • 副作用

    • useEffect 绑定函数是否重新执行和依赖的数据
    • useLayoutEffect 通 useEffect,但是执行函数的时机为 dom 更新后
  • 性能优化

    • useMome 缓存计算结果
    • useCallback 缓存函数
  • 额外的 hook

    • useRef 获取dom、获取子组件方法或保存数据
    • useImperativeHandle 将子组件方法暴露给父级

正文

分享的思路:

基于一个实际场景的功能实现,期望同学可以理解什么场景使用什么hook

案例的基本功能如图:

Untitled Diagram.drawio.png

一个列表页。

获取列表数据

useEffect 一般会作为数据请求使用到的 hook。但是我们获取数据一般为一个async函数,这里推荐使用 ahooksuseAsyncEffect

import { useState, useEffect } from "react";

let times = 0;

const data = [
  {
    text: "第一条"
  },
  {
    text: "第二条"
  }
];

const getList = () => {
  times++;
  return data.map((item) => ({
    text: `${item.text} - ${times}`,
  }));
};

const Item = ({ idx, text, onMount }) => {
  console.log(`item ${idx} 渲染`);
  return <li>{text}</li>;
};

const List = () => {
  const [list, setList] = useState([]);
  const [parentState, setParentState] = useState(0);
  
  const setListByRequest = useCallback(() => {
     const data = getList();
     setList(data); 
  }, [])

  useEffect(() => {
    setListByRequest();
  }, [setListByRequest]);
  
  const onMount = () => {}

  return (
    <>
      <span>{parentState}</span>&nbsp;
      <button onClick={() => setParentState(oldV => (oldV + 1))}>更新状态</button>
      <ul>
        {list.map(({ text }, idx) => {
          return <Item key={idx} idx={idx} onMount={onMount} text={text} />;
        })}
      </ul>
    </>
  );
};

export default function App() {
  return (
      <div className="App">
        <List />
      </div>
  );
}

useEffect 延伸

我们在使用 useEffect 做接口请求时,会把接口参数胡作为依赖项。

以分页请求为例: useEffect(updateFn, [pageNum, pageSize])

如果从 [1, 10] 变更为 [2, 10] 则 updateFn 执行

但一般我们维护接口请求的参数都是一个对象

{ pageNum: 1, pageSize: 10 }

useEffect 不会比较对象内的变量是否有变更。因此我们可以封装下 useEffect 的比较逻辑。

import { useEffect, useRef } from 'react'
import { isEqual } from 'lodash'

const useDeepDiffEffect = (updateCallback, deps) => {
    // useRef 可以用来保存数据
    // 只有通过 xxRef.current = xxx 才会影响数据
    const oldDepsRef = useRef()
    const updateFlagRef = useRef(false)
    
    // 使用 isEqual 深度比较对象内的数据是否有变化
    if (!isEqual(oldDepsRef.current, deps) {
       updateFlagRef = !updateFlagRef.current
       oldDepsRef.current = deps
    }
    
    useEffect(updateCallback, [updateFlagRef.current])
}

减少 Item 渲染

List 组件在渲染时,Item 也会随之渲染。理论上讲,应该只有在列表数据变更时,Item 才去重新渲染。

目前案例中,点击 更新状态 这个按钮。触发setParentState后,我们就可以看到Item里面的console.log信息。但实际我们并没有更改list里面的数据。

下面就用需要用到React.momeuseCallback(上面有提到)。

这里介绍下React.memo:

在参数 props 没有变更时避免组件的重新渲染官方文档

更改上方Item的代码

const Item = React.memo(({ idx, text, onMount }) => {
  console.log(`item ${idx} 渲染`);
  return <li>{text}</li>;
});

使用React.memo后,我们就需要保证props内的每个对象引用都是不变的。

但是在父组件每次从新渲染时,onMount 每次都会再声明一次,因此传递给Itemprops每次也会不一样。

因此我们可以使用 useCallback 包裹onMount函数,保证onMount 的引用维持不变,

const onMount = useCallback(() => {}, [])

现在再点击 更新状态 这个按钮。 我们现在就看不到Item里面的console.log信息了。

useCallback 延伸

useCallback 传递的 callback 函数,如果内部使用到了某一个 state,而没有声明对state的依赖。则callback执行时,获取到的state就可能是旧值。下面用一段代码解释下原因。

错误的场景

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

function App () {
    const [state, setState] = useState(0)
    
    const logState = useCallback(() => {
        console.log(state)
    }, [])
    
    useEffect(() => {
        // 这里更新 state 为 1
        setState(1)
    }, [])

    useEffect(() => {
        setTimeout(() => {
            // 这里打印的 state 为 0
            logState()
        }, 1000)
    }, [logState])
}

下面模拟下造成上面问题的原因:

let oldCallback = null;
let oldDeps = null;

const useCallback = (callback, deps = []) => {
  if (!oldCallback || !oldDeps || !oldDeps.every((item, idx) => item === deps[idx])) {
    oldCallback = callback;
    oldDeps = deps;
  }

  return oldCallback;
};

const data = {
  a: 1
}

function render() {
  let a = data.a
  
  console.log('实际值--->', a)

  const fn = useCallback(() => {
    console.log('useCallback值--->', a)
  }, []); // 对比这里我们加入 a 这个依赖

  fn()
}

render();
// 更新变量值
data.a = data.a + 1
render()

详细原因可参考维基百科-静态作用域MDN-闭包

主动刷新List

List 依赖的数据,是在组件内部发送请求获取到的。如果我们在 List 外, 因为一些特殊原因需要主动刷新List,就需要将ListsetListByRequest方法暴露外部。类似的场景譬如Antd ProComponentProTable手动触发

下面就用需要用到React.forwardrefuseRefuseImperativeHandle

这里介绍下React.forwardref:

可以接收组件的ref属性。官方文档

改造上面的代码

import Reaact, { useRef, useImperativeHandle } from 'react'

const List = React.forwardref((props, ref) => {
    ....
    useImperativeHandle(ref, () => ({
        reload: () => {
          setListByRequest();
        }
    }));x
    ...
})

export default function App() {
  const listRef = useRef();

  const onReload = () => {
    listRef?.current?.reload?.();
  };

  return (
    <div className="App">
      <div>
        <button onClick={onReload}>刷新列表</button>
      </div>
      <hr />
      <List ref={listRef} />
    </div>
  );
}

主题色

主题色是整个应用的状态,因此很容我们就会想到使用useContextuseContext是用来消费全局状态的,而创建就需要使用React.createContext。类似的场景如antdConfigProvider 全局配置

这里介绍下React.createContext:

创建一个可供子组件消费的上下文。 官方文档

React.createContext方法返回一个值中,有个Provider的属性。此属性是一个组件,接收value属性作为全局的状态。如果某个组件使用useContext消费某个Context,则需要保证该组件被Provider包裹。

改造上面的代码

import Reaact, { useContext } from 'react'

const GlobalContext = React.createContext({});

const Item = React.memo(({ idx, text, onMount }) => {
  const themeConfig = useContext(GlobalContext);
  console.log(`item ${idx} 渲染`);

  return <li style={{
    color: themeConfig.color
  }}>{text}</li>;
});

export default function App() {
  ...
  const [theme, setTheme] = useState("black");
  ...
 
  return (
    <GlobalContext.Provider
      value={{
        color: theme
      }}
    >
      <div className="App">
        <div>
          <button onClick={onReload}>刷新列表</button>
          &nbsp;
          <button onClick={() => setTheme("black")}>黑色主题</button>
          &nbsp;
          <button onClick={() => setTheme("blue")}>蓝色主题</button>
        </div>
        <hr />
        <List ref={listRef} />
      </div>
    </GlobalContext.Provider>
  );
}

完整的Demo

image.png