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>
<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>
<button
onClick={() => {
dispatch(upFont({ size: size.current.value * 1 }));
}}
>
字体变大
</button>
<button
onClick={() => {
dispatch(downFont({ size: size.current.value * 1 }));
}}
>
字体变小
</button>
<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>
<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)))
小结
优势
- context时可定义多个数据根节点,redux只有一个store根节点。
- context不用额外下载并导入redux、redux-truck包,可以略微减小打包后的文件大小。
劣势
使用context没有专属的调试工具,redux专属调试工具redux-devtools-extension功能很强大!
reducer对比
context和redux对比图
小结
优势
- context可以按数据用途划分目录,所有相同用途的reducer相关文件均放在一个文件夹下,不同数据在不同的节点,易于维护。
- context不需要导入combineReducers将多个reducer汇总。
劣势
暂时没发现!
另外:关于数据初始化使用context时我放在store里做了;使用redux一般在reducer里做。这是编码风格问题,无所谓优劣。
调用方式对比
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也难以解决问题!