【React】使用useContext + useReducer实现一个简易版状态管理工具
本文适合对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去消费。
其实上面的代码已经可以实现状态管理了。接下来我们要做的是一个把状态管理的代码抽离出来,做一个小的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来获取
最终成功实现把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
来看效果
可以,成功实现异步调用!!
结语
文章到这里就算结束了,希望看官姥爷看完给个赞呗,码字不易。git仓库我放到末尾了,感兴趣的可以拿去玩玩,也可以尝试自己再实现写redux的模块功能!大家如果按照文章代码,可能会报一些ts的错误,不用管就行(这里我懒的改了 哈哈哈 git仓库代码里ts是对的)谢谢大家
git仓库
转载自:https://juejin.cn/post/7156123099522400293