venaissance / myBlog

💡 致力于提升技术学习效率的博客
https://venaissance.github.io/myBlog
23 stars 3 forks source link

一次搞定七大 React Hooks & Hooks 的一些最佳实践分享 #15

Open venaissance opened 4 years ago

venaissance commented 4 years ago

上次的一次搞定前端四大手写在知乎上收获了500多个赞,简直让我受宠若惊。今天就趁热打铁,写一下一次搞定七大 React Hooks,一方面是为了复习下 React,另一方面是跟小伙伴们分享一些我学习 React Hooks 时的心得体会。由于水平有限,目前只能从 React Hooks 的基本使用方法使用要点上做些分享,关于 Hooks 的原理上的探究日后再做更新。

让我们先来看道字节面试题,题目是实现一个自定义的 Hook,实现点击切换状态。

function SomeComponent() {
  const [state, toggleState] = useToggle(false);
  return <div>
    {state ? 'true' : 'false'}
    <button onClick={toggleState}></button>
  </div>
}

// 请实现 useToggle
function useToggle(initialValue) {
    const [value, setValue] = useState(initialValue);
    const toggle = () => {setValue(!value)};
    return [value, toggle];
}

七大 Hooks 都有哪些

  1. useState 状态
  2. useEffect 钩子,还有它的兄弟 useLayoutEffect
  3. useContext 上下文
  4. useReducer 代替 Redux
  5. useMemo 缓存,还有它的小弟 useCallback
  6. useRef 引用
  7. 自定义 Hook 混合

useState

基本语法: const [X, setX] = React.useState(X的初始值)

简单示例:

function App() {
  const [user,setUser] = useState({name:'Varian', age: 18})
  const onClick = ()=>{
    setUser({
      name: 'Janye'
    })
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

我们会发现,点击按钮之后,age 消失了,而我们明明只改了 name 呀,为什么呢?

简单来说就是前后是两个完全不相关的对象。

展开讲的话 React 在数据变化时会创建新的虚拟 DOM 对象,然后将这个虚拟 DOM 对象跟原虚拟 DOM 进行一个 DOM Diff,得到一个最小的变化过程 Patch,并把这个 Patch 渲染到页面上,Diff 的时候发现新对象没有 age 这个属性,于是就把它删除了。

于是在使用 useState 的时候我们需要注意两个地方:

  1. 想要原来的值,必须在 setX 里先进行复制,类似这样 setUser({...user, name: 'Janye'})
  2. setX(obj) 时,obj 的地址必须改变

useEffect

useEffect 的作用主要是用来解决函数组件如何像类组件一样使用生命周期钩子的问题。

它有三个使用场景:

  1. 作为 componentDidMount 使用,第二个参数为空数组 []
  2. 作为 componentDidUpdate 使用,第二个参数为指定依赖
  3. 作为 componentWillUnmount 使用,通过 return

这里给一个最简单的例子:

const BlinkyRender = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value]);

  return (
    <div id="x" onClick={() => setValue(0)}>value: {value}</div>
  );
};

ReactDOM.render(
  <BlinkyRender />,
  document.querySelector("#root")
);

那么它跟它的兄弟 useLayoutEffect 有什么区别呢? useEffect 在浏览器渲染完成后执行,useLayoutEffect 在浏览器渲染前执行,useLayoutEffect 总是比 useEffect 先执行。

那么为了用户体验(先渲染就能先看到),通常我们应该先用useEffect

useContext

如果我们想在组件之间共享状态的话,可以使用 useContext

它的使用可以分为三个步骤:

  1. 使用C = createContext(initial) 创建上下文
  2. 使用<C.provider> 圈定作用域
  3. 在作用域内使用 useContext(C) 来使用上下文

简单示例:

const C = createContext(null);

function App() {
  console.log("App 执行了");
  const [n, setN] = useState(0);
  return (
    <C.Provider value={{ n, setN }}>
      <div className="App">
        <Baba />
      </div>
    </C.Provider>
  );
}

function Baba() {
  const { n, setN } = useContext(C);
  return (
    <div>
      我是爸爸 n: {n} <Child />
    </div>
  );
}

function Child() {
  const { n, setN } = useContext(C);
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是儿子 我得到的 n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}

useReducer

如果要一句话解释 useReducer 的话,它是用来代替 Redux 的,或者说,是一个加强版的 useState

使用上来说,一共有四步:

  1. 创建初始值 initialState
  2. 创建所有操作 reducer(state, action)
  3. 传给 useReducer,得到读和写 API
  4. 调用 写({type: '操作类型'})

这里给一个基本的示例:

const initial = {
  n: 0
};

const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown type");
  }
};

function App() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
    dispatch({ type: "add", number: 1 });
  };
  const onClick2 = () => {
    dispatch({ type: "add", number: 2 });
  };
  return (
    <div className="App">
      <h1>n: {n}</h1>

      <button onClick={onClick}>+1</button>
      <button onClick={onClick2}>+2</button>
    </div>
  );
}

useMemo

基本语法:useMemo(回调函数, [依赖])

类似与 Vue 的计算属性 computed,useMemo 具有缓存,依赖改变才重新渲染的功能。

跟它的小弟 useCallback 的唯一区别是:useMemo可以缓存所有对象,useCallback只能缓存函数。

useCallback(x => log(x), [m]) 等价于 useMemo(() => x => log(x), [m])

useRef

主要作用是创建一个数据的引用,并让这个数据在 render 过程中始终保持不变

基本语法: const count = useRef(0),读取用 count.current

用法这里给大家参考一下我封装 Echarts 时的例子:

export function ReactEcharts(props) {
  const {option, loading} = props
  const container = useRef(null)
  const chart = useRef(null)

  useEffect(() => {
    const width = document.documentElement.clientWidth
    const c = container.current
    console.log(c)
    c.style.width = `${width - 20}px`
    c.style.height = `${(width - 20) * 1.2}px`
    chart.current = echarts.init(c, 'dark')

  }, []) // [] - mounted on first time

  useEffect(() => {
    chart.current.setOption(option)
  }, [option]) // when option change 类似 vue 的 watch

  useEffect(() => {
    if (loading) chart.current.showLoading()
    else chart.current.hideLoading()
  }, [loading])
  return (
    <div ref={container}/>
  )
}

自定义 Hook

可以理解为我们可以把上面的 Hook 按照实际的需求混合起来,封装成一个函数,给一个简单示例:

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list,
    setList: setList
  };
};
export default useList;

Hooks 的一些最佳实践分享

useState

  1. 将完全不相关的 state 拆分为多组 state。比如 size 和 position
  2. 某些 state 是相互关联的,或者需要一起发生改变,就可以把它们合并为一组 state,例如:
    const [pageInfo, setPageInfo] = useState({ page: 0, pageSize: 10 });
const [saveModalVisible, setSaveModalVisible] = useState(false);
const [modalInitData, setModalInitData] = useState(null);
=>
const [saveModalInfo, setSaveModalInfo] = useState({
    visible: false,
    data: null,
});

useEffect

  1. 依赖数组依赖的值最好不要超过 3 个,否则会导致代码会难以维护
  2. 依赖项过多问题 a. 去掉不必要的依赖 b. 将 Hook 拆分为更小的单元,每个 Hook 依赖于各自的依赖数组 c. 合并相关的 state,将多个依赖值聚合为一个 d. 通过 setState 回调函数获取最新的 state,以减少外部依赖 e. 通过 ref 来读取可变变量的值,不过需要注意控制修改它的途径
  3. useEffect 和生命周期函数不做类比,useEffect 单纯用作处理副作用

    A "side effect" is anything that affects something outside the scope of the function being executed. These can be, say, a network request, which has your code communicating with a third party (and thus making the request, causing logs to be recorded, caches to be saved or updated, all sorts of effects that are outside the function.

  4. 受控组件:利用props,去修改内部的state
    
    import React, { useState, useEffect } from 'react';

interface Props { value: number, onChange: (num: number) => any }

export default function Counter({ value, onChange }: Props) { const [count, setCount] = useState(0);

useEffect(() => { value && setCount(value); }, [value]);

return [

{count}
, ] } ``` 5. 清除副作用 a. 每次副作用执行,都会返回一个新的clear函数 b. clear函数会在下一次副作用逻辑之前执行(DOM渲染完成之后) c. 组件销毁也会执行一次 ### useCallback 1. 在 useCallback 内部使用了 setState ,可以考虑使用 setState callback 减少依赖 ```js const useValues = () => { const [values, setValues] = useState({ data: {}, count: 0 }); const updateData = useCallback((nextData) => { setValues({ data: nextData, count: values.count + 1 }); // 因为 callback 内部依赖了外部的 values 变量,所以必须在依赖数组中指定它 }, [values], ); return [values, updateData]; }; ``` 可以优化为: ```js const useValues = () => { const [values, setValues] = useState({}); const updateData = useCallback((nextData) => { setValues((prevValues) => ({ data: nextData, count: prevValues.count + 1, })); // 通过 setState 回调函数获取最新的 values 状态,这时 callback 不再依赖于外部的 values 变量了,因此依赖数组中不需要指定任何值 }, []); // 这个 callback 永远不会重新创建 return [values, updateData]; }; ``` 2. 使用 ref 来保存可变变量 ```js const useValues = () => { const [values, setValues] = useState({}); const latestValues = useRef(values); useEffect(() => { latestValues.current = values; }); const [updateData] = useCallback((nextData) => { setValues({ data: nextData, count: latestValues.current.count + 1, }); }, []); return [values, updateData]; }; ``` ### useMemo 1. 需要使用的场景 a. 保持引用相等 1. 对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用 useMemo 2. 自定义 Hook 中暴露出来的 object、array、函数等,都应该使用 useMemo 。以确保当值相同时,引用不发生变化 3. 使用 Context 时,如果 Provider 的 value 中定义的值(第一层)发生了变化,即便用了 Pure Component 或者 React.memo,仍然会导致子组件 re-render。这种情况下,仍然建议使用 useMemo 保持引用的一致性 b. 成本很高的计算 2. 无需使用的场景 a. 返回的值是原始值:string, boolean, null, undefined, number, symbol b. 对于一些简单的 JS 运算(数组操作)来说,我们不需要使用 useMemo c. 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件),且没有用到其他 Hook 的依赖数组中,一般不需要使用 useMemo。 3. Hook 类型相同,且依赖数组一致时,应该合并成一个 Hook ```js const dataA = useMemo(() => { return getDataA(); }, [A, B]); const dataB = useMemo(() => { return getDataB(); }, [A, B]); // 应该合并为 const [dataA, dataB] = useMemo(() => { return [getDataA(), getDataB()] }, [A, B]); ``` 4. 在使用 useMemo 或者 useCallback 时,确保返回的函数只创建一次 错误示例: ```js export const useCount = () => { const [count, setCount] = useState(0); const [increase, decrease] = useMemo(() => { const increase = () => { setCount(count + 1); }; const decrease = () => { setCount(count - 1); }; return [increase, decrease]; }, [count]); return [count, increase, decrease]; }; ``` 在 useCount Hook 中, count 状态的改变会让 useMemo 中的 increase 和 decrease 函数被重新创建。由于闭包特性,如果这两个函数被其他 Hook 用到了,我们应该将这两个函数也添加到相应 Hook 的依赖数组中,否则就会产生 bug。比如: ``` function Counter() { const [count, increase] = useCount(); useEffect(() => { const handleClick = () => { increase(); // 执行后 count 的值永远都是 1 }; document.body.addEventListener("click", handleClick); return () => { document.body.removeEventListener("click", handleClick); }; }, []); return

{count}

; } ``` 在 useCount 中,increase 会随着 count 的变化而被重新创建。但是 increase 被重新创建之后, useEffect 并不会再次执行,所以 useEffect 中取到的 increase 永远都是首次创建时的 increase 。而首次创建时 count 的值为 0,因此无论点击多少次, count 的值永远都是 1。 解决方案: 1)通过 setState 回调,让函数不依赖外部变量 ``` export const useCount = () => { const [count, setCount] = useState(0); const [increase, decrease] = useMemo(() => { const increase = () => { setCount((latestCount) => latestCount + 1); }; const decrease = () => { setCount((latestCount) => latestCount - 1); }; return [increase, decrease]; }, []); // 保持依赖数组为空,这样 increase 和 decrease 方法都只会被创建一次 return [count, increase, decrease]; }; ``` 2)通过 ref 来保存可变变量 ``` export const useCount = () => { const [count, setCount] = useState(0); const countRef = useRef(count); useEffect(() => { countRef.current = count; }); const [increase, decrease] = useMemo(() => { const increase = () => { setCount(countRef.current + 1); }; const decrease = () => { setCount(countRef.current - 1); }; return [increase, decrease]; }, []); // 保持依赖数组为空,这样 increase 和 decrease 方法都只会被创建一次 return [count, increase, decrease]; }; ``` ### 自定义 Hook 1. 编写自定义 Hook 时,返回值一定要保持引用的一致性 ```js function Example() { const data = useData(); const [dataChanged, setDataChanged] = useState(false); useEffect(() => { setDataChanged((prevDataChanged) => !prevDataChanged); // 当 data 发生变化时,调用 setState。 // 如果 data 值相同而引用不同,就可能会产生非预期的结果。 }, [data]); console.log(dataChanged); return ; } const useData = () => { // 获取异步数据 const resp = getAsyncData([]); // 处理获取到的异步数据,这里使用了 Array.map。 // 因此,即使 data 相同,每次调用得到的引用也是不同的。 const mapper = (data) => data.map((item) => ({...item, selected: false})); return resp ? mapper(resp) : resp; }; ``` > 如果因为 prop 的值相同而引用不同,从而导致子组件发生 re-render,不一定会造成性能问题。 > 因为 Virtual DOM re-render ≠ DOM re-render。但是当子组件特别大时,Virtual DOM 的 Diff 开销也很大。因此,还是应该尽量避免子组件 re-render。 2. 自定义 Hooks 的返回值可以使用 Tuple 类型,更易于在外部重命名 ```js export const useToggle = (defaultVisible: boolean = false) => { const [visible, setVisible] = useState(defaultVisible); const show = () => setVisible(true); const hide = () => setVisible(false); return [visible, show, hide] as [typeof visible, typeof show, typeof hide]; }; const [isOpen, open, close] = useToggle(); // 在外部可以更方便地修改名字 const [visible, show, hide] = useToggle(); ``` 3. 如果返回值的数量超过三个,建议返回一个对象 ## 推荐阅读 [useEffect 完整指南](https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/) [五个大型项目实战总结,解密React Hooks最佳实践方式](https://juejin.im/post/5dde49846fb9a071aa34be81) [React Hooks 你真的用对了吗?](https://zhuanlan.zhihu.com/p/85969406) [useEffect使用指南](https://zhuanlan.zhihu.com/p/65773322) [使用 React Hooks 声明 setInterval](https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/) ## 写在最后 过去一年的一些感悟跟大家分享一下: 1. 技术的学习和提高离不开持之以恒地练习,需要不断温故知新才能克服遗忘曲线; 2. 利用好每次和面试官交流的机会,对于自己生疏的知识点进行及时反思复盘,进一步完善自己的知识体系。