React Hooks

动机(官方)

  • 组件之间很难重用有状态逻辑
  • 复杂的组件变得难以理解
  • 类 class 混淆了人和机器
  • 更符合 FP 的理解, React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数

    1. UI = render(data)
    2. UI = f(data)

    常用 hook

    useState

    1. const [state, setState] = useState(initialState)
  • useState 有一个参数,该参数可以为任意数据类型,一般用作默认值

  • useState 返回值为一个数组,数组的第一个参数需要使用的 state,第二个参数为一个 setFn。 完整例子

    1. function Love() {
    2. const [like, setLike] = useState(false)
    3. const likeFn = () => (newLike) => setLike(newLike)
    4. return (
    5. <>
    6. 你喜欢我吗: {like ? 'yes' : 'no'}
    7. <button onClick={likeFn(true)}>喜欢</button>
    8. <button onClick={likeFn(false)}>不喜欢</button>
    9. </>
    10. )
    11. }

    关于使用规则

  • 只在 React 函数中调用 Hook;

  • 不要在循环、条件或嵌套函数中调用 Hook。 来看看规则 2 为什么会有这个现象, 先看看 hook 的组成

    1. function mountWorkInProgressHook() {
    2. // 注意,单个 hook 是以对象的形式存在的
    3. var hook = {
    4. memoizedState: null,
    5. baseState: null,
    6. baseQueue: null,
    7. queue: null,
    8. next: null
    9. };
    10. if (workInProgressHook === null) {
    11. firstWorkInProgressHook = workInProgressHook = hook;
    12. /* 等价
    13. let workInProgressHook = hooks
    14. firstWorkInProgressHook = workInProgressHook
    15. */
    16. } else {
    17. workInProgressHook = workInProgressHook.next = hook;
    18. }
    19. // 返回当前的 hook
    20. return workInProgressHook;
    21. }

    每个 hook 都会有一个 next 指针,hook 对象之间以单向链表的形式相互串联, 同时也能发现 useState 底层依然是 useReducer 再看看更新阶段发生了什么

    1. // ReactFiberHooks.js
    2. const HooksDispatcherOnUpdate: Dispatcher = {
    3. // ...
    4. useState: updateState,
    5. }
    6. function updateState(initialState) {
    7. return updateReducer(basicStateReducer, initialState);
    8. }
    9. function updateReducer(reducer, initialArg, init) {
    10. const hook = updateWorkInProgressHook();
    11. const queue = hook.queue;
    12. if (numberOfReRenders > 0) {
    13. const dispatch = queue.dispatch;
    14. if (renderPhaseUpdates !== null) {
    15. // 获取Hook对象上的 queue,内部存有本次更新的一系列数据
    16. const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    17. if (firstRenderPhaseUpdate !== undefined) {
    18. renderPhaseUpdates.delete(queue);
    19. let newState = hook.memoizedState;
    20. let update = firstRenderPhaseUpdate;
    21. // 获取更新后的state
    22. do {
    23. // useState 第一个参数会被转成 useReducer
    24. const action = update.action;
    25. newState = reducer(newState, action);
    26. //按照当前链表位置更新数据
    27. update = update.next;
    28. } while (update !== null);
    29. hook.memoizedState = newState;
    30. // 返回新的 state 以及 dispatch
    31. return [newState, dispatch];
    32. }
    33. }
    34. }
    35. // ...
    36. }

    结合实际看下面一组 hooks

    1. let isMounted = false
    2. if(!isMounted) {
    3. [name, setName] = useState("张三");
    4. [age] = useState("25");
    5. isMounted = true
    6. }
    7. [sex, setSex] = useState("男");
    8. return (
    9. <button
    10. onClick={() => {
    11. setName(李四");
    12. }}
    13. >
    14. 修改姓名
    15. </button>
    16. );

    首次渲染时 hook 顺序为

    1. name => age => sex

    二次渲染的时根据上面的例子,调用的 hook 的只有一个

    1. setSex

    所以总结一下初始化阶段构建链表,更新阶段按照顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染当两次顺序不一样的时候就会造成渲染上的差异。
    为了避免出现上面这种情况可以安装 eslint-plugin-react-hooks

    1. // ESLint 配置
    2. {
    3. "plugins": [
    4. // ...
    5. "react-hooks"
    6. ],
    7. "rules": {
    8. // ...
    9. "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    10. "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
    11. }
    12. }

    useEffect

    1. useEffect(effect, array)
  • effect 每次完成渲染之后触发, 配合 array 去模拟类的生命周期

  • 如果不传,则每次 componentDidUpdate 时都会先触发 returnFunction(如果存在),再触发 effect [] 模拟 componentDidMount [id] 仅在 id 的值发生变化以后触发 清除 effect

    1. useEffect(() => {
    2. ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    3. return () => {
    4. ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    5. };
    6. });

    useLayoutEffect

    跟 useEffect 使用差不多,通过同步执行状态更新可解决一些特性场景下的页面闪烁问题 useLayoutEffect 会阻塞渲染,请谨慎使用

    1. import React, { useLayoutEffect, useEffect, useState } from 'react';
    2. import './App.css'
    3. function App() {
    4. const [value, setValue] = useState(0);
    5. useEffect(() => {
    6. if (value === 0) {
    7. setValue(10 + Math.random() * 200);
    8. }
    9. }, [value]);
    10. const test = () => {
    11. setValue(0)
    12. }
    13. const color = !value ? 'red' : 'yellow'
    14. return (
    15. <React.Fragment>
    16. <p style={{ background: color}}>value: {value}</p>
    17. <button onClick={test}>点我</button>
    18. </React.Fragment>
    19. );
    20. }
    21. export default App;

    useContext

    1. const context = useContext(Context)

    useContext 从名字上就可以看出,它是以 Hook 的方式使用 React Context, 先简单介绍 Context 的概念和使用方式

    1. import React, { useContext, useState, useEffect } from "react";
    2. const ThemeContext = React.createContext(null);
    3. const Button = () => {
    4. const { color, setColor } = React.useContext(ThemeContext);
    5. useEffect(() => {
    6. console.info("Context changed:", color);
    7. }, [color]);
    8. const handleClick = () => {
    9. console.info("handleClick");
    10. setColor(color === "blue" ? "red" : "blue");
    11. };
    12. return (
    13. <button
    14. type="button"
    15. onClick={handleClick}
    16. style={{ backgroundColor: color, color: "white" }}
    17. >
    18. toggle color in Child
    19. </button>
    20. );
    21. };
    22. // app.js
    23. const App = () => {
    24. const [color, setColor] = useState("blue");
    25. return (
    26. <ThemeContext.Provider value={{ color, setColor }}>
    27. <h3>
    28. Color in Parent: <span style={{ color: color }}>{color}</span>
    29. </h3>
    30. <Button />
    31. </ThemeContext.Provider>
    32. );
    33. };

    useReducer

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

    语法糖跟 redux 差不多,放个基础 🌰

    1. function init(initialCount) {
    2. return {count: initialCount};
    3. }
    4. function reducer(state, action) {
    5. switch (action.type) {
    6. case 'increment':
    7. return {count: state.count + 1};
    8. case 'decrement':
    9. return {count: state.count - 1};
    10. case 'reset':
    11. return init(action.payload);
    12. default:
    13. throw new Error();
    14. }
    15. }
    16. function Counter({initialCount}) {
    17. const [state, dispatch] = useReducer(reducer, initialCount, init);
    18. return (
    19. <>
    20. Count: {state.count}
    21. <button
    22. onClick={() => dispatch({type: 'reset', payload: initialCount})}>
    23. Reset
    24. </button>
    25. <button onClick={() => dispatch({type: 'increment'})}>+</button>
    26. <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    27. </>
    28. );
    29. }

    useRef

    1. const refContainer = useRef(initialValue);

    useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变

  • 解决引用问题—useRef 会在每次渲染时返回同一个 ref 对象

  • 解决一些 this 指向问题
  • 对比 createRef — 在初始化阶段两个是没区别的,但是在更新阶段两者是有区别的。
  • 在一个局部函数中,函数每一次 update,都会在把函数的变量重新生成一次。
  • 所以每更新一次组件, 就重新创建一次 ref, 这个时候继续使用 createRef 显然不合适,所以官方推出 useRef。
  • useRef 创建的 ref 仿佛就像在函数外部定义的一个全局变量,不会随着组件的更新而重新创建。但组件销毁,它也会消失,不用手动进行销毁
  • 总结下就是 ceateRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用

    useMemo

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

    一个常用来做性能优化的 hook,看个 🌰

    1. const MemoDemo = ({ count, color }) => {
    2. useEffect(() => {
    3. console.log('count effect')
    4. }, [count])
    5. const newCount = useMemo(() => {
    6. console.log('count 触发了')
    7. return Math.round(count)
    8. }, [count])
    9. const newColor = useMemo(() => {
    10. console.log('color 触发了')
    11. return color
    12. }, [color])
    13. return <div>
    14. <p>{count}</p>
    15. <p>{newCount}</p>
    16. {newColor}</div>
    17. }

    这个时候将传入的 count 值改变 的,log 执行循序

    1. count 触发了
    2. count effect
  • 可以看出有点类似 effect, 监听 a、b 的值根据值是否变化来决定是否更新 UI

  • memo 是在 DOM 更新前触发的,就像官方所说的,类比生命周期就是 shouldComponentUpdate
  • 对比 React.Memo 默认是是基于 props 的浅对比,也可以开启第二个参数进行深对比。在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。使用 useMemo 可以精细化控制,进行局部 Pure

    useCallback

    1. const memoizedCallback = useCallback(
    2. () => {
    3. doSomething(a, b);
    4. },
    5. [a, b],
    6. );
    useCallback 的用法和上面 useMemo 差不多,是专门用来缓存函数的 hooks
    下面的情况可以保证组件重新渲染得到的方法都是同一个对象,避免在传给onClick的时候每次都传不同的函数引用
    1. import React, { useState, useCallback } from 'react'
    2. function MemoCount() {
    3. const [value, setValue] = useState(0)
    4. memoSetCount = useCallback(()=>{
    5. setValue(value + 1)
    6. },[])
    7. return (
    8. <div>
    9. <button
    10. onClick={memoSetCount}
    11. >
    12. Update Count
    13. </button>
    14. <div>{value}</div>
    15. </div>
    16. )
    17. }
    18. export default MemoCount

    自定义 hooks

    自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook 一般将 hooks 分为这几类

    util

    顾名思义工具类,比如 useDebounce、useInterval、useWindowSize 等等。例如下面 useWindowSize
    1. import { useEffect, useState } from 'react';
    2. export default function useWindowSize(el) {
    3. const [windowSize, setWindowSize] = useState({
    4. width: undefined,
    5. height: undefined,
    6. });
    7. useEffect(
    8. () => {
    9. function handleResize() {
    10. setWindowSize({
    11. width: window.innerWidth,
    12. height: window.innerHeight,
    13. });
    14. }
    15. window.addEventListener('resize', handleResize);
    16. handleResize();
    17. return () => window.removeEventListener('resize', handleResize);
    18. },
    19. [el],
    20. );
    21. return windowSize;
    22. }

    API

    像之前的有一个公用的城市列表接口,在用 redux 的时候可以放在全局公用,不用的话就可能需要复制粘贴了。有了 hooks 以后只需要 use 一下就可以在其他地方复用了
    1. import { useState, useEffect } from 'react';
    2. import { getCityList } from '@/services/static';
    3. const useCityList = (params) => {
    4. const [cityList, setList] = useState([]);
    5. const [loading, setLoading] = useState(true)
    6. const getList = async () => {
    7. const { success, data } = await getCityList(params);
    8. if (success) setList(data);
    9. setLoading(false)
    10. };
    11. useEffect(
    12. () => {getList();},
    13. [],
    14. );
    15. return {
    16. cityList,
    17. loading
    18. };
    19. };
    20. export default useCityList;
    21. // bjs
    22. function App() {
    23. // ...
    24. const { cityList, loading } = useCityList()
    25. // ...
    26. }

    logic

    逻辑类,比如有一个点击用户头像关注用户或者取消关注的逻辑,可能在评论列表、用户列表都会用到,可以这样做
    1. import { useState, useEffect } from 'react';
    2. import { followUser } from '@/services/user';
    3. const useFollow = ({ accountId, isFollowing }) => {
    4. const [isFollow, setFollow] = useState(false);
    5. const [operationLoading, setLoading] = useState(false)
    6. const toggleSection = async () => {
    7. setLoading(true)
    8. const { success } = await followUser({ accountId });
    9. if (success) {
    10. setFollow(!isFollow);
    11. }
    12. setLoading(false)
    13. };
    14. useEffect(
    15. () => {
    16. setFollow(isFollowing);
    17. },
    18. [isFollowing],
    19. );
    20. return {
    21. isFollow,
    22. toggleSection,
    23. operationLoading
    24. };
    25. };
    26. export default useFollow;
    只需暴露三个参数就能满足大部分场景

    UI

    还有一些和 UI 一起绑定的 hook, 但是这里有点争议要不要和 ui 一起混用。有时候一起使用确实能解决了部分复用问题。 ```javascript import React, { useState } from ‘react’; import { Modal } from ‘antd’; // TODO 为了兼容一个页面有多个 modal, 目前想法通过唯一 key 区分,后续优化 export default function useModal(key = ‘open’) { const [opens, setOpen] = useState({
  1. [key]: false,
  2. });
  3. const onCancel = () => {
  4. setOpen({ [key]: false });
  5. };
  6. const showModal = (type = key) => {
  7. setOpen({ [type]: true });
  8. };
  9. const MyModal = (props) => {
  10. return <Modal key={key} visible={opens[key]} onCancel={onCancel} {...props} />;
  11. };
  12. return {
  13. showModal,
  14. MyModal,
  15. };

} // 使用 function App() { const { showModal, MyModal } = useModal(); return <> </> } ```

总结

越来越多的 react 配套的三方库都上了 hooks 版,像 react-router、redux 都出了 hooks。
同时也出现了一些好用的 hooks 库,比如 ahooks 这种。