React hooks & 状态管理

作者: shaokang 时间: September 21, 2022字数:6719

hooks

React Hooks 是从 React 16.8 版本推出的新特性,目的是解决 React 的 状态共享以及组件生命周期管理混乱的问题。React Hooks 的出现标志着,React 不会再存在无状态组件的情况,React 将只有类组件和函数组件的概念。

官方文档

userState 状态钩子

useState() 用于为函数组件引入状态

useState 函数入参只有一个,就是 state 的初始值,这个初始值可以是数字、字符串、对象,甚至是一个函数。useState 函数会返回一个数组,该数组包含 2 个元素:第 1 个元素为我们定义的变量,第 2 个元素为修改该变量对应的函数名称。

const [variable, setVariable] = useState(value);

setVariable(newValue); // 修改variable的值

代码示例:

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

  function increment() {
    setCount(count + 1);
  }

  function decrement() {
    // 入参为函数的情况,函数参数为上次变化的 state 结果
    setCount((prevCount) => {
      return prevCount - 1;
    });
  }

  return (
      <>
        count: {count}
        <button onClick={increment}>+</button>
        <button onClick={decrement}>-</button>
      </>
  );
}

上述的代码示例能看到 setCount 的函数可以为函数,那什么时候能用到这种方式呢?
我们知道 setState 赋值过程是异步的,因此下面的代码最终只会执行一次 +1。这个时候就可以传入函数,拿到上次的 state 来进行操作。

for (let i = 0; i < 3; i++){
  setCount(count + 1); // => setCount(prevCount => prevCount + 1);
}

如何处理数据类型为 Object 的情况?
在类组件中,setState 是执行的是 “异步对比累加赋值”。但 useState 定义的修改函数 setXxxx,执行的是 “异步直接赋值”。所以正确的做法是将完成的对象全部传入,如下示例

setPerson({...person, age: 18});

useEffect 副作用钩子

在类组件中有 componentDidMount(组件被挂载完成后)、componentDidUpdate(组件重新渲染完成后)、componentWillUnmount(组件即将被卸载前) 生命周期,hooks 提供了 useEffect 钩子补齐了函数组件的 “生命周期”。

useEffect(effect,[deps]) 函数可以传入 2 个参数,第 1 个参数为我们定义的执行函数、第 2 个参数是依赖关系(可选参数)。若一个函数组件中定义了多个 useEffect,那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的。 代码形式:

useEffect(() => {
  //此处编写 组件挂载之后和组件重新渲染之后执行的代码
  ...

  return () => {
      //此处编写 组件即将被卸载前执行的代码
      ...
  }
},[deps])

‘[deps]’补充说明:

  1. 若缺省,则组件挂载、组件重新渲染、组件即将被卸载前,每一次都会触发该 useEffect;
  2. 若传值,则必须为数组,数组的内容是函数组件中通过 useState 自定义的变量或者是父组件传值过来的 props 中的变量,告诉 React 只有数组内的变量发生变化时才会触发 useEffect;
  3. 若传值,但是传的是空数组 [],则表示该 useEffect 里的内容仅会在 “挂载完成后和组件即将被卸载前” 执行一次;

代码示例:

  // 1. 每当状态改变时,都要重新执行 useEffect 的逻辑:
  useEffect(() => {
    switchCount += 1
  });

  // 2. 每次状态都改变,也只执行第一次 useEffect 的逻辑:
  useEffect(() => {
    switchCount += 1
  }, []);

  // 3. 根据某个状态是否变化来决定要不要重新执行:
  useEffect(() => {
    switchCount += 1
  }, [value]);

  // 4. 组件卸载时处理一些内存问题,比如清除定时器、清除事件监听:
  useEffect(() => {
    const handler = () => {
      document.title = Math.random().toString()
    }

    window.addEventListener('resize', handler)

    return () => {
      window.removeEventListener('resize', handler)
    }
  }, []);

React 18 后 useEffect 执行了两次

useContext 共享状态钩子

useContext 是 React Hook 提供的跨级组件数据传递的一种方式,可以很方便的去订阅上下文的改变,并在合适的时候重新渲染组件。useContext 的使用方式如下:

function Component(){
  const value = useContext(MyContext); // 在函数组件中声明一个变量来代表该共享数据对象的 value 值

  return <div>{value.xxx}</div>
}

子组件(函数组件)需要先引入共享数据对象 MyContext,使用示例:

import React, { useContext } from 'react';
// 定义在全局下
const AppContext = React.createContext(); // 也可以设置默认值,如 React.createContext({name: 'Yang',age: 18});假如没有设置值,就会使用 Provider 上面定义的默认值

function AppComponent() {
    <AppContext.Provider value=>
        <Component />
    </AppContext.Provider>
  </div>
}

function Component(){
  return <ChildComponent />
}

function ChildComponent(){
  // 这里还可以使用 <AppContext.Consumer> 包裹组件来获取值,不过这种写法太麻烦了
  const { name, age } = useContext(AppContext);
  return <div>{name} - {age}</div>
}

useReducer Action 钩子

React 状态管理工具 Redux 由 Action、Reducer 和 Store 三个对象构成,而 Reducer 是唯一可以更新组件中 State 的途径。
useReducer 是 React Hooks 提供的一个函数,主要用来在某些复杂的场景中替换 useState。例如,包含复杂逻辑的 state 且包含多个子值,或者后面的 state 依赖于前面的 state 等。

useReducer 的语法格式如下:

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

// 有时候需要惰性地创建初始 state,将将创建函数作为 init 传入第三个函数
function init(initialCount) {
    return {count: initialCount};
}

使用示例:

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 源码中,useState 就是由 useReducer 实现的。我们还可以用 useReducer + useContext 实现 Redux 一样的效果。

什么时候用 useState / useReducer?
组件自己内部的简单逻辑变量用 useState、多个组件之间共享的复杂逻辑变量用 useReducer。

状态管理

React 的状态管理主要分三类:Local state、Context、第三方库。

Local State

React v16.8 之后,函数组件开发模式成为主流,hooks 丰富了函数组件的众多能力。 Local State 通过 useState 和 useReducer 来管理

useState - 更细粒度的状态

组件状态通过 useState 来进行管理 – useState hooks 将组件的状态进行更细粒度的拆分.

import {useState} from 'react';

const Foo = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </>
  );
}

useReducer - 复杂逻辑抽象和复用

当 state 计算逻辑复杂或者存在共性时,我们可以优先考虑 useReducer.

例如常见的列表分页逻辑封装:

import { useReducer } from 'react';

const initialState = { pageNum: 1, pageSize: 15 };

const reducer = (state, action) => {
    switch (action.type) {
        case 'next': // 下一页
            return { ...state, pageNum: state.pageNum + 1 };
        case 'prev': // 前一页
            return { ...state, pageNum: state.pageNum - 1 };
        case 'changePage': // 跳转到某页
            return { ...state, pageNum: action.payload };
        case 'changeSize': // 更改每页展示条目数
            return { pageNum: 1, pageSize: action.payload };
        default:
            return state;
    }
};

const Page = () => {
    const [pager, dispatch] = useReducer(reducer, initialState);

    return (
        <Table
            pageNum={pager.pageNum}
            pageSize={pager.pageSize}
            onGoNext={() => dispatch({ type: 'next' })}
            onGoPrev={() => dispatch({ type: 'prev' })}
            onPageNumChange={num =>
                dispatch({ type: 'changePage', payload: num })
            }
            onPageSizeChange={size =>
                dispatch({ type: 'changeSize', payload: size })
            }
        />
    );
};

Context

文档介绍

使用 useContext + useReducer 来避免透传 prop 的情况。

import { createContext, useReducer, useContext } from 'react';

const ParentDispatch = createContext(null);

const Parent = () => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <ParentDispatch.Provider value={dispatch}>
            <DeepTree parentState={state} />
        </ParentDispatch.Provider>
    );
};

// 深层子组件
const DeepChild = () => {
    const dispatch = useContext(ParentDispatch);

    const handleClick = () => {
        dispatch({ type: 'add', payload: 'hello' });
    };

    return <button onClick={handleClick}>Add</button>;
};

状态管理库

常见的 React 状态管理库如 Redux、Mobx、Recoil. 参考文档

最新的一些状态管理库 Zustand、Recoil、Jotai,对比:2023 再谈前端状态管理