tt-up / fed-in-depth

经验、知识、笔记——让坚持学习成为一种习惯
6 stars 1 forks source link

react的几种状态管理模式 #27

Open chentianyuan opened 3 years ago

chentianyuan commented 3 years ago

react中的状态管理

状态管理是前端开发中不可避免的问题,react自身提供了几种常用的状态管理方式,但是在一些B端项目,需要复杂状态管理的场景中(例如复杂表单,Tab切换等)react自带的状态管理往往不足以满足我们的需求,因此除此之外还有一些常见的状态管理辅助工具,帮助项目更好地维护复杂的业务状态。

1、使用props传值的形式

这种就不介绍了,简单易用,但是缺点也很明显,遇到多层组件嵌套,兄弟组件传值时,层层传递会造成代码冗余,组件可用性降低,状态传递混乱。

2、children透传

在父组件通过props.children向孙组件传递状态,Parent组件得以更加独立,复用性更强。不过这种形式在没有嵌套的兄弟组件传值时就显得没那么方便了。

function Grandpa () {
  const [count, setCount] = useState(0)
  return (
    {/* 通过prop children向子组件传递props */}
    <Parent>
      <Child count={coun} setCount={setCount}></Child>
    </Parent>
  )
}

function Parent (props) {
  return (
    <div>
      {props.children}
    </div>
  )
}

function Child (props) {
  const { count, setCount } = props
  return (
    <div onClick={() => setCount(currentCount => currentCount + 1)}>
      { count }
    </div>
  )
}

3、Context传递

通过Provider包裹的子组件都能通过useContext来获取最近的Provider所传递的values,在状态管理复杂度没有那么高的情况下(如小型MPA项目),以上三种react自带支持的状态管理方式已经可以很好地满足日常开发需求。但是遇到大型项目时context的管理方式会面临臃肿,状态变更来源不清晰,命名冲突等一些常见的问题,此时便需要我们的辅助登场了。

const CountContext = React.createContext(null)

function Grandpa () {
  const [count, setCount] = useState(0)
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <Parent></Parent>
    </CountContext.Provider>
  )
}

function Parent () {
  const { count, setCount } = useContext(CountContext)
  return (
    <div onClick={() => setCount(currentCount => currentCount + 1)}>
      { count }
    </div>
  )
}

4、redux? useReducer!

redux只是一种辅助工具,在16.8之后的react中,因为useReducer的存在,我们甚至不需要引入redux来辅助进行状态管理(redux的一些工具函数,例如combineReducers自己实现的话)。

import React, { useCallback } from 'react'

const INPUTCHANGE = 'INPUTCHANGE'
const ADDTOITEM = 'ADDTOITEM'
const CLEARITEM = 'CLEARITEM'

function storeReset () {
  return {
    current: '',
    todoList: []
  }
}

const reducer = function (state, action) {
  switch (action.type) {
    case INPUTCHANGE:
      return {
        ...state,
        current: action.payload
      }
    case ADDTOITEM:
      return {
        ...state,
        current: '',
        todoList: [...state.todoList, action.payload]
      }
    case CLEARITEM:
      return {
        ...state,
        ...storeReset()
      }
    default:
      return state
  }
}

const createAction = function (type, payload) {
  return {
    type,
    payload
  }
}

export default function ChildUseReducer(props) {
  const [state, disptch] = React.useReducer(reducer, null, storeReset)

  const handInputChange = useCallback(value => {
    disptch(createAction(INPUTCHANGE, value))
  }, [])

  const handAddToItem = useCallback(value => {
    if (!value) return
    disptch(createAction(ADDTOITEM, value))
  }, [])

  const handClearItem = useCallback(() => {
    disptch(createAction(CLEARITEM))
  }, [])

  return (
    <section className={'useReducer'}>
      <input onChange={e => handInputChange(e.target.value)} value={state.current}/>
      <button onClick={() => handAddToItem(state.current)}>添加</button>
      <button onClick={() => handClearItem()}>清空</button>
      <div>{ state.todoList.map((item, i) => {
        return <li key={i}>{ item }</li>
      }) }</div>
    </section>
  )
}

上面是一个简单的redux使用方式,可以看到redux中的数据状态变更只能通过disptch一个action对象来对reducer中的initstate进行更改。如果我们将该state和disptch通过context.provider传递到每个子组件,在最外层维护我们的reduce和action,就可以实现全局的状态管理,也因为同一管理解决了命名冲突和状态变更来源不清晰的问题。

5、redux-thunk || useThunkReducer!

useThunkReducer源码地址

上面的例子在状态管理方面已经做得很好了,但是唯一的问题是,我们disptch的必须是一个action对象,如此一来,我们必须在组件中进行数据的获取,但是数据的获取和变更其实总是分不开的,我们需要将异步获取和数据变更聚合起来,将行为和动作,do what和how区分开来。

useThunkReducer hook解析

// 1.函数接收我们的reducer,defaultArgs和一个初始化函数
export function useThunkReducer(reducer, initialArg, init = (a) => a) {
  // 2.使用useState订阅初始状态,保证使用到的组件在数据变动时能正确更新
  const [hookState, setHookState] = useState(() => init(initialArg));

  // 3.记录hookState,防止二次创建对象带来的消耗
  const state = useRef(hookState);
  // 4.缓存getState方法,防止二次创建
  const getState = useCallback(() => state.current, [state]);
  // 5.创建并缓存setState方法,用于更新state
  const setState = useCallback((newState) => {
    state.current = newState;
    setHookState(newState);
  }, [state, setHookState]);

  // 6.缓存reduce函数,避免重复创建
  const reduce = useCallback((action) => {
    return reducer(getState(), action);
  }, [reducer, getState]);

  // 7.加载器,兼容了action是对象和函数的形式,当action是一个函数时,将会执行函数,并且将自身递归传递给我们的action函数,这样使得我们可以将我们的异步逻辑抽离到action中,并且在第一次抽离的action中执行disptch时还可以继续传递fn形式的action,便于不同的业务逻辑拆分。在最后通过disptch一个对象类型的action时才会真正地执行setState(reducer(getState(), action)),得到经由reducer函数操作后的state,由setState更新。
  const dispatch = useCallback((action) => {
    return typeof action === 'function'
      ? action(dispatch, getState)
      : setState(reduce(action));
  }, [getState, setState, reduce]);

  return [hookState, dispatch];
}
// Root Component
const Context = createContext(null);
const [state, dispatch] = useThunkReducer(reducer, undefined, reducer);
<Context.Provider value={{ dispatch, state }}>
  {/* ... */}
</Context.Provider>
// Child Component
// 通过context获取到所有的state和包装后的disptch函数,然后就可以快乐地封装我们自己的异步状态变更的逻辑,然后使用这个封装好的disptch函数来执行它了
const context = useContext(Context);
const { state, dispatch } = context;

disptch(myTestAction())
// Action
const myTestAction = () => async (disptch, getState) => {
  disptch(createAction(LOADING, true))
  const data = await fetchMockData()
  disptch(createAction(UPDATE_MOCK_DATA, data))
  disptch(createAction(LOADING, false))
}

总结

⭐️:状态管理复杂程度

1、props ⭐️

2、children render ⭐️

3、context provider ⭐️⭐️

4、redux + useReducer ⭐️⭐️⭐️

5、redux + (redux-thunk | useThunkReducer) ⭐️⭐️⭐️⭐️

react中常见的几种状态管理方式,可以依据项目状态复杂度按需食用。

yuqingc commented 3 years ago

有一点不明白,感觉没有必要使用 context,为啥不可以直接在组件中使用这个代码?是为了让代码量更少吗(看起来并没有减少)

const [state, dispatch] = useReducer(reducer, undefined, useThunkReducer);
chentianyuan commented 3 years ago

有一点不明白,感觉没有必要使用 context,为啥不可以直接在组件中使用这个代码?是为了让代码量更少吗(看起来并没有减少)

const [state, dispatch] = useReducer(reducer, undefined, useThunkReducer);

已经把state获取改成懒加载const [hookState, setHookState] = useState(() => init(initialArg)); 可以使用const [state, dispatch] = useThunkReducer(reducer, undefined, reducer);来获取state和disptch 不过还是感觉context中取更符合直觉一点,而且无需重新import useThunkReducer和reducer模块

yuqingc commented 3 years ago

而且无需重新import useThunkReducer和reducer模块

为了避免重复引用,可以写一个自定义 hook,包装一波