likes
comments
collection
share

【React】使用useContext + useReducer实现一个简易版状态管理工具

作者站长头像
站长
· 阅读数 18

本文适合对hooks有一定了解的朋友。主要在于使用useContext和useReducer自制实现一个简易版的redux。会从下面几个方向来讲解。1.useContext和useReducer的作用 2.如何结合使用 3.练习一个小demo

前言

截止目前,我相信各位读者都接触了很多的hooks,什么useState,useRef不计其数。而观看这篇文章的,有可能是首页推荐点进来观看,想看看如何使用这两个hooks来实现状态管理的,或者说是想学习这两个hooks的使用的。今天,我会先通过讲解这两个hooks,然后通过一个小demo来帮助大家学习。如果文章中有什么不对的地方,欢迎评论区指出哈!!

useContext + useReducer的介绍

useContext

const value = useContext(MyContext);

useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

别忘记 useContext 的参数必须是 context 对象本身

  • 正确:  useContext(MyContext)
  • 错误:  useContext(MyContext.Consumer)
  • 错误:  useContext(MyContext.Provider) 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化

提示

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

把如下代码与 Context.Provider 放在一起

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);  return (    <button style={{ background: theme.background, color: theme.foreground }}>      I am styled by theme context!    </button>  );
}

useContext是获取Context中提供的数据,之前没有useContext的时候,我们通过context.Consumer获取是很繁琐的,而且维护性和可读性很差

useReducer

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

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

以下是用 reducer 重写 useState 一节的计数器示例:

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 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch

指定初始 state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:

  const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}  );

注意

React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用 useReducer(reducer, undefined, reducer) 来模拟 Redux 的行为,但我们不鼓励你这么做。

惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:

function init(initialCount) {  return {count: initialCount};}
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':      return init(action.payload);    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useContext + useReducer结合使用实现状态管理

通过上面的内容,我们简单了解了下useContext和useReducer的作用。相信大部分人还是有点懵。没关系,接下来我们结合使用,帮助大家更好理解一下。

首先useContext,能够消费createContext中提供的状态,而useReducer,能够更好的管理我们的状态。如果我们把useReducer的数据和dispatch,通过context传递下去,然后在组件中通过useContext消费,就能够实现简单的状态管理了,例如:

// App.tsx
import './App.css';
import React, { useContext, useReducer } from 'react'
import Son from './pages/Son';
const initState = {
  count: 1
} // 定义初始化数据
export const Context = React.createContext<{
  state: typeof initState,
  dispatch: React.Dispatch<any>
}>({
  state: initState,
  dispatch: () => { },
})


function App() {
  const reducer = (preState, action) => {
    let { type } = action;
    if (typeof action === 'function') {
      type = action()
    }

    switch (type) {
      case 'increment':
        return { count: preState.count + 1 };
      default:
        return preState;
    }
  } // 定义reducer
  const [state, dispatch] = useReducer(reducer, initState) // 把数据传递给reducer

  return (
    <Context.Provider value={{ state, dispatch }}>
      <div className="App">
        <div>这是一个组件</div>
        <Son />
      </div>
    </Context.Provider>
  );
}

export default App;
// Son.tsx
import React, { useContext } from 'react';
import { Context } from '../App'
const Son = () => {
  const context = useContext(Context)
  console.log(context);

  return (
    <div>
      这是子组件
      {context.state.count}
      <div onClick={() => context.dispatch({ type: 'increment' })}>子组件里点击count + 1</div>
    </div>
  )
}
export default Son

我们定义了两个组件,一个是App,一个是Son。在App组件中通过Context传递useReducer的状态,然后在Son组件通过useContext去消费。

【React】使用useContext + useReducer实现一个简易版状态管理工具 其实上面的代码已经可以实现状态管理了。接下来我们要做的是一个把状态管理的代码抽离出来,做一个小的demo。

简易redux

做一个简易的redux

在上面代码中,我们的状态其实都保存在组件中,不利于维护,所以我们第一步要把状态代码抽离出来

1.抽离状态代码,新建store文件夹

在store文件夹下新建index文件,把与公共状态相关的代码放进去

import React, { createContext, Dispatch, ReactPropTypes, useCallback, useContext, useReducer } from 'react';
interface Prop {
  a?: 1,
  children?: JSX.Element
}
type Props = Prop & ReactPropTypes;
interface IState {
  count: number;
}

type IcontextDis = Dispatch<Action> | ((prop: {
  type: string;
  payload: any
}) => void)
interface IContext {
  state: IState;
  dispatch: IcontextDis
}
type Action = {
  type?: 'string',
  payload: any
} | (() => Promise<any>)
const initState = {
  count: 1
}
const Context = createContext<IContext>({
  state: initState,
  dispatch: () => { },
});
export const useCount = () => {
  return useContext(Context)
}
export const ContextProvider: React.FC<Props> = (props) => {
  const reducer = useCallback((preState, action) => {
    let { type } = action;
    switch (type) {
      case 'increment':
        return { count: preState.count + 1 };
      default:
        return preState;
    }
  }, []);
  const [state, dispatch] = useReducer(reducer, initState)
  return (
    <Context.Provider value={{ state, dispatch }}>
      {props.children}
    </Context.Provider>
  )
}
export default ContextProvider

2.在Src下面的index文件中导入ContextProvider 并使用该组件包裹App组件,用来提供状态

这样 整个项目中都可以使用公共状态

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {ContextProvider} from './store'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <>
    <ContextProvider>
    <App />
    </ContextProvider>
  </>
);

3.需要用到公共组件的数据,使用useCount获取

// App文件 获取count的值
import './App.css';
import React from 'react'
import Son from './pages/Son';
import { useCount } from './store';

function App() {
  const context = useCount()
  return (
    <div className="App">
      <div>这是一个组件{context.state.count}</div>
      <Son />
    </div>
  );
}

export default App;



// Son文件 点击按钮更改count值
import React from 'react';
import { useCount } from '../store';
const Son = () => {
  const context = useCount()
  return (
    <div>
      <div onClick={() => context.dispatch({ type: 'increment' })}>子组件里点击count + 1</div>
    </div>
  )
}
export default Son

我们在App文件展示count的值,在son文件更改count值,获取和修改都通过store 里面暴露的useCount来获取

【React】使用useContext + useReducer实现一个简易版状态管理工具 最终成功实现把useReducer抽离出来

其实到这里,一个简易的redux算完成了。只不过redux里面还有分模块和dispatch里面写函数的功能。这里分模块的功能我就不去实现了,其实分模块并没有太大必要。在前端项目中,我个人人为不要把所有数据都放在公共状态里面。最好是状态让组件自己去管理,这样能减少项目占用的内存。除非是一些经常用到的,例如我最近写的一个项目,项目中的tips文案,都是后端返回的,这些数据每个组件都可能用,才会放公共状态里面。

接下来,让我们实现一下dispatch写函数的功能

4.dispatch可以传入一个函数

dispatch如果要实现传入一个函数,我们可以对useReducer返回的dispatch进行进一步封装

// store index文件下面,新增个funDispatch
const [state, dispatch] = useReducer(reducer, initState)

  const funcDispatch: React.Dispatch<Action> = (action: Action) => {
  // 判断action是不是函数,如果是函数,就执行,并且把dispatch传进去
    if (typeof action === 'function') {
      action(dispatch)
    } else {
      dispatch(action)
    }
  }
  return (
    <Context.Provider value={{ state, dispatch: funcDispatch }}>
      {props.children}
    </Context.Provider>
  )

改写一下Son里面代码,让dispatch执行一个函数,函数3秒后再dispatch,模仿发请求的异步任务

import React, { Dispatch } from 'react';
import { Action, useCount } from '../store';
const Son = () => {
  const context = useCount()
  const add1 = (dispatch: Dispatch<Action>) => {
    // 3秒后执行
    setTimeout(() => {
      dispatch({ type: 'increment' })
    }, 3000)
  }
  return (
    <div>
      <div onClick={() => context.dispatch(add1)}>子组件里点击count + 1</div>
    </div>
  )
}
export default Son

来看效果

【React】使用useContext + useReducer实现一个简易版状态管理工具 可以,成功实现异步调用!!

结语

文章到这里就算结束了,希望看官姥爷看完给个赞呗,码字不易。git仓库我放到末尾了,感兴趣的可以拿去玩玩,也可以尝试自己再实现写redux的模块功能!大家如果按照文章代码,可能会报一些ts的错误,不用管就行(这里我懒的改了 哈哈哈 git仓库代码里ts是对的)谢谢大家

git仓库

useReducer学习仓库代码-实现简易redux