React 基础 - 状态管理

前言

本文会逐步讲解React项目中的状态管理方式及实现方式。并顺便手撸一个UmiuseModal

单状态 useState

父组件定义state,通过props向子组件传递,子组件共享父组件的状态,修改状态后,所有子组件同步更新。

tsx 复制代码
function Parent1() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count => count + 1)
  }
  return (
    <div>
      <Parent1_Child1 count={count} handleClick={handleClick}/>
      <Parent1_Child1 count={count} handleClick={handleClick}/>
    </div>
  )
}
function Parent1_Child1(props: {count: number, handleClick: () => void}) { 
  const { count, handleClick } = props
  return <button type='button' onClick={handleClick}>{ count }</button>
}

多状态 useReducer

使用 useReducer 定义一个对象,写一个reducer函数 通过dispatch方法传递的actionType判断执行逻辑,从而修改 state

tsx 复制代码
type Parent2StateType = {
  val1: number,
  val2: number,
}
type ActionType<T extends keyof Parent2StateType = keyof Parent2StateType> = {
  [K in T]: {
    type: K;
    value: Parent2StateType[K];
  }
}[T] | { type: 'reset', value?: Parent2StateType }

const defaultState: Parent2StateType = {
  val1: 0,
  val2: 0,
}
const parent2Reducer = (state: Parent2StateType, action: ActionType) => { 
  if (action.type === 'reset') return action.value || defaultState
  return {
    ...state,
    [action.type]: action.value,
  }
}
function Parent2() {
  const [values, dispatch] = useReducer<React.Reducer<Parent2StateType, ActionType>>(
    parent2Reducer,
    defaultState
  )

  return (
    <div>
      <Parent2_Child1 count={values.val1} handleClick={() => dispatch({ type: 'val1', value: values.val1 + 1 })}/>
      <Parent2_Child1 count={values.val2} handleClick={() => dispatch({ type: 'val2', value: values.val2 + 1 })}/>
    </div>
  )
}

上面的例子,看上去很麻烦,小伙伴便想:我将state定义成一个对象,也使用类似于reducer更新函数的方式修改state,那么和useReducer有什么区别么?

答案是:没什么区别。。。。附源码:

ts 复制代码
function rerenderReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  ...
  if (lastRenderPhaseUpdate !== null) {
    ...
    do {
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== firstRenderPhaseUpdate);
    ...
    hook.memoizedState = newState;
    ...
    queue.lastRenderedState = newState;
  }
  return [newState, dispatch];
}
对比 useStateuseReducer
  • 对于大量的state维护,useReducer会有更少的代码量。
  • useReducer整合state修改逻辑到reducer方法中,逻辑更加清晰,更方便调试。

(说得很好,我使用 useState,实际应用中,我更倾向于按业务分离hooks来分别维护)

跨组件传递 useState + useContext

上述示例,state 都只能在组件内使用,或者传递给直接子组件使用。如果有多层子组件,或者有多个子组件,那么会涉及到大量的props传递逻辑,有没有办法,不通过props传递,可以直接在任意子组件中获取到最新的state呢?

答案便是结合useContext

tsx 复制代码
const Parent3Context = createContext<{count: number, handleClick:() => void}>()

function Parent3() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count => count + 1)
  }
  return (
    <Parent3Context.Provider value={{ count, handleClick }}>
      <Parent3_Child1 />
      <Parent3_Child2 />
    </Parent3Context.Provider>
  )
}

function Parent3_Child1() { 
  const { count, handleClick } = useContext(Parent3Context)
  return <button type='button' onClick={handleClick}>{ count }</button>
}
function Parent3_Child2() {
  return <Parent3_Child1 />
}

可以看到,如果我们将state放到context中,将会给子组件赋予跨层级获取state的能力,而且不需要将state层层传递。

于是有小伙伴就想了:那太好了,以后我一个页面就一个context,将所有state都塞进去,想在哪里用就在哪里取,再也不用层层传递props了。

我们来看下一个DEMO

tsx 复制代码
function Parent4() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count => count + 1)
  }
  return (
    <Parent4Context.Provider value={{ count, handleClick }}>
      parent: {Math.random()} <br/>
      <Parent4_Child1 /><br/>
      <Parent4_Child2 /><br/>
      <Parent4_Child3 /><br/>
    </Parent4Context.Provider>
  )
}

function Parent4_Child1() { 
  const { count, handleClick } = useContext(Parent4Context)
  return <>
    Parent4_Child1: {Math.random()} <br/>
    <button type='button' onClick={handleClick}>{count}</button>
  </>
}
function Parent4_Child2() {
  return <>
    Parent4_Child2: {Math.random()} <br/>
    <Parent4_Child1 />
  </>
}
function Parent4_Child3() {
  return <>
    Parent4_Child3: {Math.random()} <br/>
  </>
}

我们在render里面添加随机数方法,如果随机数变化了,说明组件进行了重渲染。通过修改状态可以发现:只要状态改变了,那么Provider下面所有的子组件都会进行重渲染,和是否调用useContext没任何关系。

这样的话,如果我们将整个页面的state都放在一个context里面,就相当于将所有的state都定义在页面顶层,任意state变化,都会导致整个页面重新渲染。这样来说,对性能影响是极大的。

我们希望:

  • state定义能和页面层隔离开,方便维护。
  • state变化仅影响当前获取state的组件,不影响其它组件

这里我们将state定义提取成useCount5hook 组件,仅负责定义和赋值逻辑。将provider 提取成Parent5Context组件,负责加入context。

tsx 复制代码
const Parent5Context = createContext<{ count: number, handleClick: () => void }>()

const useCount5 = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)
  return { count, handleClick }
}

function Parent5Provider(props: React.PropsWithChildren) { 
  const { count, handleClick } = useCount5()
  return (
    <Parent5Context.Provider value={{ count, handleClick }}>
      { props.children }
    </Parent5Context.Provider>
  )
}

function Parent5() {
  return (
    <Parent5Provider>
      parent: {Math.random()} <br/>
      <Parent5_Child1 /><br/>
      <Parent5_Child2 /><br/>
      <Parent5_Child3 /><br/>
    </Parent5Provider>
  )
}

如此修改后,可以通过自定义多个hooks来维护多个state,再统一加入到context中,Parent5将不会因为state修改而重渲染,但是provider下面的所有每次state变化后重渲染。继续修改:

  • 将state拆分为根state和子组件自己的state
  • 根state每次更新,主动更新context中的state缓存,并且触发context中的回调集合
  • 子组件state定义在自定义hook中,默认取值从context中获取state缓存,并将set函数添加到context中的回调集合中
  • 如此之后,context相当于只是根state与多个子组件state之间的桥梁,负责同步数据,本身不会变化。
tsx 复制代码
// context修改为`不会变化`,内部缓存state值,供 update 方法读取
const Parent6ContextData = {
  // 子组件更新状态的回调函数集合
  callbacks: new Set<(newState: any) => void>(),
  // 缓存state
  data: null as any,
  // 子组件状态更新方法
  update: () => Parent6ContextData.callbacks.forEach(callback => callback(Parent6ContextData.data)),
}

const Parent6Context = createContext(Parent6ContextData)

// 定义状态 hook
const useCount6 = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)
  return { count, handleClick }
}
// 封装自定义hook,用来读取并返回最新state,并将set方法赋值到context中,用于context中更新state
const useParent6Modal = () => { 
  const context = useContext(Parent6Context)
  // 默认将state设置为context中缓存的 state
  const [state, setState] = useState(context.data)
  // 将更新方法添加到 context 的子组件状态更新回调集合中
  useEffect(() => { 
    function update(newState: any) {
      setState(newState)
    }
    context.callbacks.add(update)
    return () => {
      context.callbacks.delete(update)
    }
  }, [])
  return state
}

// 将根state放到与 children 同一级,避免state更新影响 children 重渲染
const Parent6Modal = () => {
  const context = useContext(Parent6Context)
  const state = useCount6()
  context.data = state
  // Parent6Modal 的 state变化时, 执行context的update,用来更新所有 useParent6Modal 中的 state
  useEffect(() => {
    context.update()
  }, [state])
  return null
}
function Parent6Provider(props: React.PropsWithChildren) { 
  return (
    <Parent6Context.Provider value={Parent6ContextData}>
      <Parent6Modal />
      { props.children }
    </Parent6Context.Provider>
  )
}
function Parent6() {
  return (
    <Parent6Provider>
      parent: {Math.random()} <br/>
      <Parent6_Child1 /><br/>
      <Parent6_Child2 /><br/>
      <Parent6_Child3 /><br/>
    </Parent6Provider>
  )
}

function Parent6_Child1() { 
  const { count, handleClick } = useParent6Modal()
  return <>
    Parent6_Child1: {Math.random()} <br/>
    <button type='button' onClick={handleClick}>{count}</button>
  </>
}
function Parent6_Child2() {
  return <>
    Parent6_Child2: {Math.random()} <br/>
    <Parent6_Child1 />
  </>
}
function Parent6_Child3() {
  return <>
    Parent6_Child3: {Math.random()} <br/>
  </>
}

从上述例子可以看到,所有我们不希望重渲染的随机数,在state变化时已经不会重渲染了。整体逻辑:

  • 根组件维护state,将整个state缓存到context 中
  • 子组件state默认取值为 context 中缓存的state
  • 任意子组件修改state,触发的是根state的set方法
  • 根state变化,触发执行context的回调集合
  • context的回调集合,缓存的是子组件state的set方法
  • 所以,任意子组件触发更新state,会导致所有用到state的其它子组件更新,而不影响其它未用到state的组件、

但是如此修改后,添加多个state集合会比较麻烦,涉及多处修改,所以我们将modal的维护抽离出来:

tsx 复制代码
// 将对象使用类的方式构建,内部取值更优雅, 将data和callbacks,修改为键值对,键名为model名称
class Parent7ContextClass<K extends keyof ModalTypes, S extends ReturnType<ModalTypes[K]>> { 
  callbacks: Record<K, Set<(newState: any) => void>> = {} as Record<K, Set<(newState: any) => void>>
  data: Record<K, S> = {} as Record<K, S>
  update = (name: K) => (this.callbacks[name] || []).forEach(callback => callback(this.data[name]))
}
const Parent7ContextData = new Parent7ContextClass()
const Parent7Context = createContext(Parent7ContextData)

// 添加入参为 model名称,内部配合context结构修改
const useParent7Modal = <K extends keyof ModalTypes, S extends ReturnType<ModalTypes[K]>> (name: K): S => { 
  const context = useContext(Parent7Context)
  const [state, setState] = useState<S>(context.data[name] as S)
  useEffect(() => { 
    function update(newState: S) {
      setState(newState)
    }
    context.callbacks[name] ??= new Set()
    context.callbacks[name].add(update)
    return () => {
      context.callbacks[name].delete(update)
    }
  }, [])
  return state
}

// 添加入参为 model名称,和hook函数
const Parent7Modal = <K extends keyof ModalTypes>(props: { name: K, useHook: ModalTypes[K] }) => {
  const { name, useHook } = props
  const context = useContext(Parent7Context)
  const state = useHook()
  context.data[name] = state
  useEffect(() => {
    context.update(name)
  }, [state])
  return null
}

// 定义状态 hook
const useCount7_1 = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)
  return { count, handleClick }
}
const useCount7_2 = () => {
  const [count2, setCount] = useState(0)
  const handleClick = () => setCount(count2 + 1)
  return { count2, handleClick }
}
// 定义hooks函数集合
const modals = {
  useCount7_1,
  useCount7_2,
} as const

type ModalTypes = typeof modals
// 已hooks函数集合做循环渲染
function Parent7Provider(props: React.PropsWithChildren) { 
  return (
    <Parent7Context.Provider value={Parent7ContextData}>
      {Object.entries(modals).map(([name, useHook]) => { 
        return <Parent7Modal key={name} name={name as keyof ModalTypes} useHook={useHook} />
      })}
      { props.children }
    </Parent7Context.Provider>
  )
}
function Parent7() {
  return (
    <Parent7Provider>
      parent: {Math.random()} <br/>
      <Parent7_Child1 /><br/>
      <Parent7_Child2 /><br/>
      <Parent7_Child3 /><br/>
    </Parent7Provider>
  )
}

// 修改入参为hook名称
function Parent7_Child1() { 
  const { count, handleClick } = useParent7Modal('useCount7_1')
  return <>
    Parent7_Child1: {Math.random()} <br/>
    <button type='button' onClick={handleClick}>{count}</button>
  </>
}
function Parent7_Child2() {
  return <>
    Parent7_Child2: {Math.random()} <br/>
    <Parent7_Child1 />
  </>
}
function Parent7_Child3() {
  return <>
    Parent7_Child3: {Math.random()} <br/>
  </>
}

如此之后,我们只需要写 自定义hooks,然后把 hooks 添加到 modals 中,便可以在provider下的任意子组件中调用 useParent7Modal 传递 hook的名称,即可访问到最新state,但是这个方法每次state更新都会将所有依赖state的子组件都更新,有没有办法按指定依赖更新呢,有办法的:

  • 我们useParent7Modal传入第二可选参数selector为一个函数,入参为state本身,出参为 类state
  • 在组件更新方法中深比较 新state 和 selector返回值,不同则执行update,否则不执行
tsx 复制代码
const useParent8Modal = <K extends keyof ModalTypes, S extends ReturnType<ModalTypes[K]>> (name: K, selector?: (state: S) => any): S => { 
  const context = useContext(Parent7Context)
  const [state, setState] = useState<S>(context.data[name] as S)
  const stateRef = useRef<typeof state>(state)
  useEffect(() => { 
    function update(newState: S) {
      newState = selector?.(newState) || newState
      if (!isEqual(newState, stateRef.current)) { 
        setState(newState)
        stateRef.current = newState
      }
    }
    context.callbacks[name] ??= new Set()
    context.callbacks[name].add(update)
    return () => {
      context.callbacks[name].delete(update)
    }
  }, [])
  return state
}

// 使用
const {count} = useParent8Modal('useCount7_1', model => ({count: model.count}))

以上,也是UMI脚手架中 useModel插件的完整逻辑

当然,state的维护不只有这一种方法,比如hoxRedux,但是基本都是以context作为state变化的桥梁。

完整代码

src/modals/useCount.tsx

tsx 复制代码
import { useState } from 'react'

const useCount = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)
  return { count, handleClick }
}

export default useCount

src/modals/index.ts

ts 复制代码
import useCount from './useCount'

const modals = {
  useCount,
} as const

export default modals

src/modals/provider.ts

tsx 复制代码
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import isEqual from 'fast-deep-equal'
import modals from '.'

type ModalTypes = typeof modals

class ModelContextClass<K extends keyof ModalTypes, S extends ReturnType<ModalTypes[K]>> { 
  callbacks: Record<K, Set<(newState: any) => void>> = {} as Record<K, Set<(newState: any) => void>>
  data: Record<K, S> = {} as Record<K, S>
  update = (name: K) => (this.callbacks[name] || []).forEach(callback => callback(this.data[name]))
}
const defaultModelContext = new ModelContextClass()

const ModelContext = createContext(defaultModelContext)

export const Modal = <K extends keyof ModalTypes>(props: { name: K, useHook: ModalTypes[K] }) => {
  const { name, useHook } = props
  const context = useContext(ModelContext)
  const state = useHook()
  context.data[name] = state

  useEffect(() => {
    context.update(name)
  }, [state])

  return null
}

function ModelProvider(props: React.PropsWithChildren) { 
  return (
    <ModelContext.Provider value={defaultModelContext}>
      {Object.entries(modals).map(([name, useHook]) => { 
        return <Modal key={name} name={name as keyof ModalTypes} useHook={useHook} />
      })}
      { props.children }
    </ModelContext.Provider>
  )
}

export const useModal = <K extends keyof ModalTypes, S extends ReturnType<ModalTypes[K]>> (name: K, selector?: (state: S) => any): S => { 
  const context = useContext(ModelContext)
  const [state, setState] = useState<S>(context.data[name] as S)
  const stateRef = useRef<typeof state>(state)
  useEffect(() => { 
    function update(newState: S) {
      newState = selector?.(newState) || newState
      if (!isEqual(newState, stateRef.current)) { 
        setState(newState)
        stateRef.current = newState
      }
    }
    context.callbacks[name] ??= new Set()
    context.callbacks[name].add(update)
    return () => {
      context.callbacks[name].delete(update)
    }
  }, [])
  return state
}

export default ModelProvider

app.tsx

tsx 复制代码
import ModelProvider from '@/modals/provider.ts'

function App(props) {
  return (
    <ModelProvider>
      {props.children}
    </ModelProvider>
  )
}

anyChild.tsx

tsx 复制代码
import { useModal } from '@/modals/provider.ts'

function Child() {
  const { count, handleClick} = useModal('useCount')
  return <button type='button' onClick={handleClick}>{count}</button>
}
相关推荐
秃顶老男孩.3 小时前
异步处理(前端面试)
前端·面试·职场和发展
三脚猫的喵3 小时前
微信小程序中实现AI对话、生成3D图像并使用xr-frame演示
前端·javascript·ai作画·微信小程序
文心快码BaiduComate4 小时前
文心快码3.5S全新升级,体验多智能体协同开发,最高赢无人机!
前端·后端·程序员
安卓开发者4 小时前
鸿蒙Next ArkWeb进程解析:多进程架构如何提升Web体验
前端·架构·harmonyos
炒毛豆4 小时前
移动端响应式px转换插件PostCSS的使用~
前端·javascript·postcss
恋猫de小郭4 小时前
Flutter Riverpod 3.0 发布,大规模重构下的全新状态管理框架
android·前端·flutter
wordbaby4 小时前
用 window.matchMedia 实现高级响应式开发:API 全面解析与实战技巧
前端·javascript
薄雾晚晴4 小时前
Rspack 实战,构建流程升级:自动版本管理 + 命令行美化 + dist 压缩,一键输出生产包
前端·javascript
huabuyu4 小时前
在 Taro 小程序中实现完整 Markdown 渲染器的实践
前端