内容简介:之前和大家分享了我们不需要系列:接下来我们继续调整难度, 替换应用范围更广的库:《我们或许不需要...》系列如果是做轮子就没有意义了, 此系列目的是通过简单的代码, 对原有库的设计思路进行概括提取, 最终从
之前和大家分享了我们不需要系列:
接下来我们继续调整难度, 替换应用范围更广的库: redux
, react-redux
《我们或许不需要...》系列如果是做轮子就没有意义了, 此系列目的是通过简单的代码, 对原有库的设计思路进行概括提取, 最终从 理解其理念 到 更高效 的开发项目的过程.
此行目的
Redux 在 React 中的重要性在此不再暂开, 其设计理念(单向数据流, 提供者模式)深得人心, 但是在实际开发中, 每个引用 Redux 的项目都会需要解决以下三个问题:
- 如何设计状态管理在工程中的模块结构
- 如何在不影响设计结构的前提下减少编写 action, reducer 的模板代码
- 如何减少不必要的重绘(immutable)
我们最后会基于 Context, 使用 20 行代码和一点点规范满足以上目标
本文中提到的代码都可以直接粘贴至项目中进行验证.
redux 官方最佳实践
首先编写项目入口
// src/index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import Home from './Home';
import ChangerBar from './ChangerBar';
import store from './store';
// 我们在项目的最外层包裹一个 Provider 对象, 以实现提供者模式
function App() {
return (
<Provider store={store}>
<div>
<ChangerBar />
<Home />
</div>
</Provider>
);
}
render(<App />, document.getElementById('root'));
复制代码
接下来初始化状态, 并且编写 reducer, 根据后续 dispatch 传递的 action 对象修改状态
// src/store.js
import { createStore } from 'redux';
const defaultStore = {
name: 'dog',
friends: ['cat', 'fish'],
age: 100,
};
function reducer(store = defaultStore, action) {
switch (action.type) {
case 'changeName':
store = { ...store, name: action.name };
break;
case 'addAge':
store = { ...store, age: store.age + 1 };
break;
default:
break;
}
return store;
}
const store = createStore(reducer);
store.dispatch({ type: 'init' });
export default store;
复制代码
接下来我们创建 actions 文件, 里面是 action 的集合
// src/actions.js
export function changeName(name) {
return {
type: 'changeName',
name,
};
}
export function addAge() {
return {
type: 'addAge',
};
}
复制代码
我们绘制一个组件, 用来修改全局状态
react-redux 提供了一个 connect 组件, 它其实是一个 HOC(高阶组件), redux 这样的设计目的有两个:
- 监听和释放对 store 的订阅的行为被封装在 HOC 中, 这样可以不必每次都编写此逻辑代码;
- 将 state 和 disptch 对象转换为 props 注入至组件中, 而不是由组件去引用外部的对象, 这样组件内部只有一个概念就是 props.
// src/ChangerBar.js
import React from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
const ChangerBar = ({ changeName, addAge }) => {
function handleOnChange(e) {
changeName(e.target.value);
}
return (
<div>
<div>bar</div>
<input placeholder="修改姓名" onChange={handleOnChange} />
<button type="button" onClick={addAge}>
更新年龄
</button>
</div>
);
};
// 将 dispatch 注入到组件Props中
function mapDispatchToProps(dispatch) {
return {
addAge: () => dispatch(actions.addAge()),
changeName: name => dispatch(actions.changeName(name)),
};
}
export default connect(
null,
mapDispatchToProps,
)(ChangerBar);
复制代码
实现 Home 组件, 订阅数据, 验证状态管理(UI 更新)
// src/Home.js
import React from 'react';
import { connect } from 'react-redux';
const Home = ({ name, age }) => {
return (
<div>
<div>name: {name}</div>
<div>age: {age}</div>
</div>
);
};
// 将 state 的值注入到 props 中
function mapStateToProps(state) {
// 当任何一个 dispatch() 执行时, 此处将会重新运行, 并且注入新的 name 和 age 至组件 props 中, 以更新组件
return {
name: state.name,
age: state.age,
};
}
export default connect(mapStateToProps)(Home);
复制代码
以上代码相信有一定经验的 React 开发者已经非常熟悉, 当项目逐渐复杂时, 我们会逐步修改项目结构, 常见的有两种:
- 鸭子模式, 将状态管理分布在一个个页面中, 跨页面的状态管理提升至全局, dva 使用的是此模式, 每个需要使用全局状态的页面都会有一个自己的 action 和 reducer
- 中心化的状态管理: 将整个 actions 和 reducer 规整到一个全局目录中, actions 以事件为约定, 而不是以页面
这两种方式各有千秋, 鸭子模式的缺点是我们无法避免有跨页面的状态管理情况发生, 所以状态会被分布在全局和局部两处.
以上模式还有一个弊端就是 action.type 我们需要保持一致, 当 action 过多时, 可能需要一个 types 的文件用来存储 action.type 常量, 如此一来我们每编写一个新的状态需要:
- 打开 types 文件, 添加一个 action.type 常量
- 打开某个 action 文件, 引入 types, 编写 action
- 打开某个 reducer 文件, 引入 types, 编写 reducer
- 打开容器组件文件, 引入 connect, actions, 编写状态的获取和触发更新
以上还是没有引入 sage 和 immutable , 写到这里已经感受到我们 react 开发者正处于水深火热之中, 我们得加紧步伐.
接下来我们抛弃 redux 重写以上代码
利用 context 实现 react-redux 类似功能
redux 作者在 context API 更新之后, 提到过, 有了 context 我们可以不需要 redux 了, 所言非虚.
我们创建一个类似 createStore 的函数, 此函数会创建一个 Provider 和一个 store, 我们要利用不可变数据减少不必要的的重绘, 这里使用 immer:
// src/createContextRedux.js
import React, { createContext, useMemo } from 'react';
import immer from 'immer';
export default function createContextRedux() {
// 创建一个 context, 用于后续配合 useContext 进行更新组件
const store = createContext();
// 创建一个提供者组件
const Provider = ({ defaultState = {}, ...rest }) => {
const [state, setState] = React.useState(defaultState);
// 仅有 state 变更了, 才会重新更新 context 和 store
return useMemo(() => {
// 使用 immer 进行更新状态, 确保未更新的对象还是旧的引用
const dispatch = fn => setState(immer(state, v => fn(v)));
store.state = state;
store.dispatch = dispatch;
return <store.Provider value={state} {...rest} />;
}, [state]);
};
return { Provider, store };
}
复制代码
好的, 这 20 行代码就是状态管理库的全部, 接下来我们利用它去实现刚刚的业务
重写 store, 我们引入刚刚编写的状态管理库, 然后创建全局 Provider 和 store:
// src/store.js
import createContextRedux from './createContextRedux';
const { Provider, store } = createContextRedux();
export { Provider, store };
复制代码
在项目最顶层使用 Provider 包裹, 以提供 context
// index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from './store';
import Home from './Home';
import ChangerBar from './ChangerBar';
function App() {
return (
<Provider>
<ChangerBar />
<Home />
</Provider>
);
}
render(<App />, document.getElementById('root'));
复制代码
这里我们移除了 reducer, 只有 action 的概念, 可以简化非常多的代码量
我修改 actions.js 文件, 由于当前只有 action 的概念, 所以非常适合使用 action 中心化的方式, 将容器组件的 action 都汇集放置一处, 容器组件仅读取状态和调用 action
// src/actions.js
import { store } from './store';
export function changeName(name) {
// 我们直接修改状态对象即可, 该函数会利用 immer 创建一个新的对象返回, 没有被修改的子对象还是旧的引用
store.dispatch(state => {
state.name = name;
});
}
export function addAge() {
store.dispatch(state => {
if (state.age === void 0) {
state.age = 0;
}
state.age += 1;
});
}
复制代码
ChangerBar 不需要关联状态, 它只需要引用 actions 即可
// src/ChangerBar.js
import React from 'react';
import * as actions from './actions';
const ChangerBar = () => {
// 使用 hook 获取 context, 代替 conncet
function handleOnChange(e) {
actions.changeName(e.target.value);
}
function handleAddAage() {
actions.addAge();
}
return (
<div>
<div>bar</div>
<input placeholder="修改姓名" onChange={handleOnChange} />
<button type="button" onClick={handleAddAage}>
更新年龄
</button>
</div>
);
};
export default ChangerBar;
复制代码
最后在 Home 页面读取全局数据, 监听全局修改, 使用 useContext 代替 connect
// src/Home.js
import React from 'react';
import { store } from './store';
const Home = () => {
const { name, age } = React.useContext(store);
return (
<div>
<div>name: {name}</div>
<div>age: {age}</div>
</div>
);
};
export default Home;
复制代码
序
有时候序也可以写在结尾, 不是么?
上面可能贴的代码太多了, 并且较为分散, 我们把它聚合成两个文件重新阅读:
实现状态管理
import React, { createContext, useMemo } from 'react';
import immer from 'immer';
export default function createContextRedux() {
// 创建一个 context, 用于后续配合 useContext 进行更新组件
const store = createContext();
// 创建一个提供者组件
const Provider = ({ defaultState = {}, ...rest }) => {
const [state, setState] = React.useState(defaultState);
// 仅有 state 变更了, 才会重新更新 context 和 store
return useMemo(() => {
// 使用 immer 进行更新状态, 确保未更新的对象还是旧的引用
const dispatch = fn => setState(immer(state, v => fn(v)));
store.state = state;
store.dispatch = dispatch;
return <store.Provider value={state} {...rest} />;
}, [state]);
};
return { Provider, store };
}
复制代码
使用状态管理
// src/index.js
import React from 'react';
import { render } from 'react-dom';
import createContextRedux from './createStore';
const { Provider, store } = createContextRedux();
// 模拟一个异步
function fetchData() {
return new Promise(res => {
setTimeout(() => {
res();
}, 500);
});
}
// 一个基础的 action, 用来修改状态
// 在实际项目中, action 应该统一放置一处, 不应该分散在各组件中
async function actionOfAddNum() {
await fetchData();
store.dispatch(state => {
state.age += 1;
});
}
// 点击之后, 利用 action 修改全局状态
function Changer() {
return (
<button type="button" onClick={actionOfAddNum}>
add
</button>
);
}
// 利用 useContext 监听全局状态, 并随时进行更新
function Shower() {
const { age } = React.useContext(store);
return <div>age: {age}</div>;
}
function App() {
return (
<Provider defaultState={{ age: 0 }}>
<Shower />
<Changer />
</Provider>
);
}
render(<App />, document.getElementById('root'));
复制代码
最终我们移除了 redux, 在确保 单向数据流
的状态逻辑上, 移除了 reducer, connect 的步骤, 将模板代码减少至一份: 编写 action
;
通过分离 action 和组件, 解耦状态变更和更新.
谢谢观看
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。