使用React官方Hooks代替redux完整方案

本文探讨了如何仅使用React官方Hooks实现Redux类似功能,通过创建自定义Context和useReducer,展示了如何在小型项目中管理共享数据。相比Redux,这种方法代码量更少,且易于理解和维护。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

redux大家都知道,它是第三方的react状态管理库,被很多人吐槽既难用又不得不用。我最近发现完全可以只用react官方hooks实现redux的大部分功能!使用起来比redux简便一些,代码量也少一下。
在这里我写了一个完整的demo,一般中小型项目完全可以参考它,实现只用官方Hooks代替redux。以下是完整步骤和代码:

新建项目

使用官方模板创建react项目:

create-react-app demo

清理public/目录

删除该目录下所有文件,然后在该目录下新建index.html,内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Demo</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

清理src/目录

删除该目录下所有文件,然后在该目录下新建index.js和app.js,内容如下:

index.js
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
    <App />,
  document.getElementById('root')
);
app.js
import Demo from "./components";
import Context from "./context";

export default function App() {
  return (
    // 使用context组件包住需要共享数据的子组件
      <Context>
        <Demo />
      </Context>
  );
}

context

在src/下创建context目录。它是整个项目需要共享数据和操作数据的容器。

context入口

首先在src/context目录下创建index.js文件,它是整个context的入口,在这里用到了createContext和useReducer这两个react hook,在这里做的事情相当于定义redux的store。

index.js
import { createContext, useReducer } from "react";
// 可以在一个context中引入多套reducer
import Color from './Font/reducer'
import Food from './Food/reducer'

// 创建 context
export const MyContext = createContext();

/**
 * 创建Context组件,该组件可以包含多套reducer,每一套reducer中都有相应的数据和动作
 * 假如子组件需要context中color相关的数据时,可以通过调用colorContext访问数据,通过调用colorDispatch更新数据
 * 同理可知子组件需要context中food相关的数据时,可以通过调用foodContext访问数据,通过调用foodDispatch更新数据
 */
export default function Context(props) {
    const [colorContext, colorDispatch] = useReducer(Color, { color: "blue", font: 20, size: 1 })  // color数据和动作初始化
    const [foodContext, foodDispatch] = useReducer(Food, { count: 10 },)  // food数据和动作初始化
    return (
        <MyContext.Provider value={{ colorContext, colorDispatch, foodContext, foodDispatch }}>
            {props.children}
        </MyContext.Provider>
    );
};

创建数据和动作目录

在这个demo中很多数据和动作需要被共享使用,这里按照数据功能划分创建了2个子目录。

创建Font目录

该目录位于src/context/Font,该目录存放的是调整字体相关的数据和动作。在该目录下有3个文件,分别存放常量、动作、处理者。

constant.js
export const UPDATE_COLOR = "updateColor"
export const UP_FONT = "upFont"
export const DOWN_FONT = "downFont"
action.js
// 专门为reducer生成action对象
import { UPDATE_COLOR, UP_FONT, DOWN_FONT } from './constant'

// 同步action
export const updateColor = data => ({ type: UPDATE_COLOR, data })
export const upFont = data => ({ type: UP_FONT, data })
export const downFont = data => ({ type: DOWN_FONT, data })

// 异步action
export const upFontAsync = (dispatch, data, time) => {
    setTimeout(() => {
        dispatch(data)
    }, time)
}
reducer.js
import { UPDATE_COLOR ,UP_FONT,DOWN_FONT} from './constant'

// 定义reducer
export default function reducer(state, action) {
    switch (action.type) {
        case UPDATE_COLOR:
            return { ...state, color: action.data.color }  // 当state中存放多个值时,先展开state,然后更新color键值对,再存回state
        case UP_FONT:
            return { ...state, font: state["font"] + action.data.size }
        case DOWN_FONT:
            return { ...state, font: state["font"] - action.data.size }
        default:
            return state
    }
}

创建Food目录

该目录位于src/context/Food,该目录存放的是调整食物相关的数据和动作。在该目录下有3个文件,分别存放常量、动作、处理者。

constant.js
export const INCREASE = "Increase"
export const DECREASE = "Decrease"
action.js
// 专门为reducer生成action对象
import { INCREASE ,DECREASE} from './constant'

// 同步action
export const increase = data => ({ type: INCREASE, data })
export const decrease = data => ({ type: DECREASE, data })
reducer.js
import { INCREASE ,DECREASE} from './constant'

// 定义reducer
export default function reducer(state, action) {
    switch (action.type) {
        case INCREASE:
            return { count: state["count"]+1 }
        case DECREASE:
            return state["count"]>1?{ count: state["count"]-1 }:{ count: 0 }
        default:
            return state
    }
}

components

在src/目录下创建components目录,这里存放了所有的jsx文件。因为这是个demo,所以不再划分page、container、router。

文件清单

index.jsx
import Font from "./Font";
import ShowArea from "./ShowArea";
import Food from "./Food";

export default function Demo() {
    return (
        <>
            <ShowArea />
            <Font />
            <Food />
        </>
    );
}
ShowArea.jsx
import React, { useContext } from "react";
import { MyContext } from "../context";

const ShowArea = (props) => {
    console.log("这里是ShowArea")
    const { colorContext } = useContext(MyContext);  //  从context中获取数据
    const { foodContext } = useContext(MyContext);  //  从context中获取数据
    // 从context中获取指定的值要使用["键"]的方式
    return (
        <>
            <div style={{ color: colorContext["color"], fontSize: colorContext["font"] }}>字体颜色展示为{colorContext["color"]}</div>
            <div>肉包目前还有{foodContext["count"]}个!</div>
        </>
    );
}

export default ShowArea
Font.jsx
import React, { useContext, useRef, useMemo } from "react";
import { MyContext } from "../context";
import { updateColor, upFont, downFont, upFontAsync } from '../context/Font/action.js'

const Inner = (props) => {
    console.log("这里是ChangeFont渲染")
    const { color, dispatch } = props;
    let size = useRef();
    return (
        <>
            <br />
            <button
                onClick={() => {
                    dispatch(updateColor({ color: "red" }));
                }}
            >
                红色
            </button>
            &nbsp;
            <button
                onClick={() => {
                    dispatch(updateColor({ color: "blue" }));
                }}
            >
                蓝色
            </button>
            <br />
            <br />
            <select ref={size}>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
            </select>
            &nbsp;
            <button
                onClick={() => {
                    dispatch(upFont({ size: size.current.value * 1 }));
                }}
            >
                字体变大
            </button>
            &nbsp;
            <button
                onClick={() => {
                    dispatch(downFont({ size: size.current.value * 1 }));
                }}
            >
                字体变小
            </button>
            &nbsp;
            <button
                onClick={() => {
                    upFontAsync(dispatch, upFont({ size: size.current.value * 1 }), 1000)
                }}
            >
                异步字体变大
            </button>
            <br />
            <div style={{ color: color["color"], fontSize: color["font"] }}>字体颜色展示为{color["color"]}</div>
        </>
    )
}

const Font = (props) => {
    const { colorContext, colorDispatch } = useContext(MyContext);
    return useMemo(
        () => {
            return (
                <>
                    <Inner color={colorContext} dispatch={colorDispatch} />
                </>)
        }
        , [colorContext, colorDispatch])
}

export default Font;
Food.jsx
import React, { useContext, useMemo } from "react";
import { MyContext } from "../context";
import { increase, decrease } from '../context/Food/action'

const Inner = (props) => {
    console.log("这里是food渲染")
    const { food, dispatch } = props
    return (
        <>
            <br />
            <br />
            <button
                onClick={() => {
                    dispatch(increase({ color: "red" }));
                }}
            >
                增加肉包
            </button>
            &nbsp;
            <button
                onClick={() => {
                    dispatch(decrease({ color: "blue" }));
                }}
            >
                减少肉包
            </button>
            <br />
            <div>肉包目前还有{food["count"]}个!</div>
        </>
    )
}

const Food = (props) => {
    const { foodContext, foodDispatch } = useContext(MyContext);
    return useMemo(
        () => {
            return (
                <>
                    <Inner food={foodContext} dispatch={foodDispatch} />
                </>)
        }
        , [foodContext, foodDispatch])
}

export default Food;

context与redux使用对比

store对比

context

import { createContext, useReducer } from "react";
import Color from './Font/reducer'
import Food from './Food/reducer'

export const MyContext = createContext();

export default function Context(props) {
    const [colorContext, colorDispatch] = useReducer(Color, { color: "blue", font: 20, size: 1 })  // color数据和动作初始化
    const [foodContext, foodDispatch] = useReducer(Food, { count: 10 },)  // food数据和动作初始化
    return (
        <MyContext.Provider value={{ colorContext, colorDispatch, foodContext, foodDispatch }}>
            {props.children}
        </MyContext.Provider>
    );
};

redux

import {createStore,applyMiddleware} from 'redux'
import reducer from './reducers'
import thunk from 'redux-thunk'
import {composeWithDevTools} from 'redux-devtools-extension'
export default createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))

小结

优势
  1. context时可定义多个数据根节点,redux只有一个store根节点。
  2. context不用额外下载并导入redux、redux-truck包,可以略微减小打包后的文件大小。
劣势

使用context没有专属的调试工具,redux专属调试工具redux-devtools-extension功能很强大!

reducer对比

context和redux对比图

在这里插入图片描述

小结

优势
  1. context可以按数据用途划分目录,所有相同用途的reducer相关文件均放在一个文件夹下,不同数据在不同的节点,易于维护。
  2. context不需要导入combineReducers将多个reducer汇总。
劣势

暂时没发现!
另外:关于数据初始化使用context时我放在store里做了;使用redux一般在reducer里做。这是编码风格问题,无所谓优劣。

调用方式对比

context

context使用说明图

  • 请注意三个红色框的部分,用自定义的Context组件包围子组件,在这些子组件中就可以获取到context中的数据和动作。
  • 再看绿色框部分,先在定义context的index.js文件中使用react的createContext定义context组件名,然后在子组件中使用react的useContext获取指定的数据集和对应的动作集!

redux

在这里插入图片描述
redux大家用的比较多,相信也比较熟悉,redux使用数据的方式是用connct将store中的数据和方法接入到state中。

小结

这部分谈不上优劣,仅仅是书写方式的差异。如果非要比个优劣的话,那context稍微有点优势。使用方式和useState一脉相承,符合大家的习惯;redux的使用方式有点奇葩,不看文档绝对想不到这样用的。

context的性能优化

前面的部分很多人都有写过,可能少有人像我这样直接拿两者逐条比较优劣。在conten性能优化这部分内容少有靠谱的文章细说一种既方便又好用的方法。在这里我仔细说一说使用context时性能的优化。
关于优化的代码上面其实已经有了。这里再贴一下图,要看文本代码的请往前找。
在这里插入图片描述
性能优化的思路是将组件拆成2个部分,容器组件负责引入context数据和动作,UI组件负责渲染。使用useMemo监控context数据和动作是否有产生变化,若有变化那么才会调用UI组件重新渲染组件,若没有变化则不会调用UI组件重新渲染。
需要注意的是useMemo监控数据是否变化也是有开销的,假如当前组件内容不多,那就没有必要使用useMemo监控了!
再看下图:
在这里插入图片描述
这里一个上面的div显示font组件相关的信息,下面一个div显示food组件的相关信息。假如使用useMemo监控数据,确实可以做到font组件相关数据变化时只刷新上面那个div、food组件相关数据变化时只刷新下面那个div,但实际上却会造成性能下降,而且增加了很多代码量。
总之,请掌握好优化方法,把握好优化尺度。假如某组件中嵌套多个子组件,重新渲染开销较大,那么应该用useMemo进行优化。反之若某组件重新渲染开销很小,那么就没必要用useMemo进行优化!!!

最后

经过认真的比较和梳理之后,我发现redux只有调试工具这一项占优势,除此以外完全可以使用useContext、createContext、useReducer、useMemo代替redux!!!用react官方hooks代码量更少、生成文件更小、官方工具在可靠性、可维护性更佳!!!
请问认真看完整篇文章的你同意我的观点么,使用React官方Hooks完全代替redux!
当然,确实存在也有一些项目复杂到使用redux难以胜任,那种情况下使用这4个hooks也难以解决问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值