Store 为什么需要被平替?
首先,Store (e.g Redux、Mobx)是为了管理大型项目的全局状态的,Store 为所有的全局状态提供了可预测,可操作,流程化的 API 方法。
但是,是不是所有的全局状态都需要存放在 Store 中呢?
答案是否定的。对于一些简单的全局状态或者是只是短暂使用到的全局状态,我们没有必要经过诸如派发器 ,注解 、装饰器这样的一些操作较为复杂的流程。
这个时候,Store 的平替方案就派上作用了,它的目的也显而易见:
- 我需要一些全局状态 & 能够修改全局状态的方法
- 我不想要太复杂的操作。
Store 的常见平替方案:
1. 使用 context API 来实现自定义 hook
使用 context-API + 内置的 react-hook 就能很简单地解决这样的问题,举个登录的例子:
- 先注册一个 Context
js
import { createContext } from 'react';
export const LoginContext = createContext({
username: '',
password: '',
token: '',
dispatch: (type, payload) => {}
});
- 写一个自定义 hook
useLogin
(这里使用的是useState
封装,也可以使用useReducer
)
js
import { useState, useEffect } from 'react';
export function useLogin() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [token, setToken] = useState('');
function dispatch(type, payload) {
switch (type) {
case 'setUsername':
// do something
break;
case 'setPassword':
// do something
break;
case 'setToken':
// do something
break;
default:
break;
}
}
useEffect(() => {
setToken(window.localStorage.getItem('token'));
}, []);
useEffect(() => {
// 校验登录 ...
}, []);
return {
username,
password,
dispatch
};
}
- 在 App 组件中,使用
useLogin
自定义 hook
jsx
import React from 'react';
import { LoginContext } from './contexts';
function App() {
const loginContextValue = useLogin();
return (
<LoginContext.Provider value={loginContextValue}>
{/* 其他内容 */}
</LoginContext.Provider>
);
}
export default App;
- 在子组件中使用:
jsx
import { useContext } from 'react';
import { LoginContext } from '@/contexts';
function Child() {
const { username, password, dispatch } = useContext(LoginContext);
return (
<div>{/*渲染内容*/}</div>
);
}
2. 使用自定义 hook : 封装一个 useCtxModel
(推荐)
直接使用 react 内置的 hook 有时候还是不能满足我们对于集成性数据操作的期望。
这个时候,封装一个自定义 hook 是比较合适的。下面的演示是为了阐述如何使用一个自定义 hook 来对 Model 进行操作:
2.1. 使用步骤:
- 声明一个
CtxModel
:
js
import { CtxModel } from 'my-hooks';
export const ctxModel = new CtxModel({
state: {
username: '',
password: '',
},
actions: {
setUsername(state, payload) {
state.username = payload;
},
setPassword(state, payload) {
state.password = payload;
},
replaceState(state, newState) {
Object.assign(state, newState);
}
},
});
- 在父组件中使用
useCtxModel
获取ctxModel
中的 Provider,并把它们包裹起来:
jsx
/* 在父组件中挂载 */
import React from 'react';
import { CtxModel, useCtxModel } from 'my-hooks';
import { ctxModel } from '@/models';
function App() {
const {
username,
password,
setUsername,
setPassword,
CtxModelProvider
} = useCtxModel(ctxModel);
return (
<CtxModelProvider>
{/* 需要渲染的内容 */}
</CtxModelProvider>
);
}
export default App;
- 在子组件中获取
ctxModel
中真实存储的值或者<Consumer>
, 并渲染到视图中:
jsx
/* 在子组件中使用 */
import React, { useContext } from 'react';
import { ctxModel } from '@/models';
import { useCtxModelContext } from 'my-hooks';
function Child1() {
const ctxModelValue = useContext(ctxModel.getContext());
const {
username,
password,
setUsername,
setPassword,
} = ctxModelValue;
return (
<div>{JSON.stringify(ctxModelValue)}</div>
);
}
function Child2() {
const CtxModelConsumer = ctxModel.getModelConsumer();
return (
<CtxModelConsumer>
{ctxModelValue => {
return (
<div>{JSON.stringify(ctxModelValue)}</div>
);
}}
</CtxModelConsumer>
);
}
function Child3() {
const ctxModelValue = useCtxModelContext(ctxModel);
/*
const {
username,
password,
setUsername,
setPassword,
} = ctxModelValue;
*/
return (
<div>{JSON.stringify(ctxModelValue)}</div>
);
}
export {
Child1,
Child2,
Child3
};
2.2. 代码实现
shell
my-hooks
|- hooks
|- useCtxModel.js
|- useCtxModelContext.js
|- index.js
|- models
|- CtxModel.js
|- index.js
|- index.js
- 从 my-hooks 中导入这些组件
js
/* my-hooks */
export {
// ... sub hooks
useCtxModel,
useCtxModelContext
} from './hooks';
export {
// ... sub models
CtxModel
} from './models';
- 封装
useCtxModel
js
import { useState, useMemo } from 'react';
import _ from 'lodash';
function useCtxModel(model) {
const initialState = model.getState();
const actions = model.getActions();
const [state, setState] = useState(initialState);
const mapActionsMethods = useMemo(() => {
const methods = {};
for (let actionType in actions) {
if (actions.hasOwnProperty(actionType)) {
methods[actionType] = (payload) => {
setState(state => {
try {
const _state = _.cloneDeep(state);
actions[actionType](_state, payload);
return _state;
} catch (e) {
console.error(e);
return state;
}
});
};
}
}
return methods;
}, [actions]);
const CtxModelProvider = useMemo(() => {
const Context = model.getContext();
const Provider = Context.Provider;
const value = {
...state,
...mapActionsMethods
};
return ({ children }) => (
<Provider value={value}>{children}</Provider>
);
}, [model, state, mapActionsMethods]);
return {
...state,
...mapActionsMethods,
CtxModelProvider
};
}
export default useCtxModel;
- 封装
useCtxModelContext
js
import { useContext } from 'react';
function useCtxModelContext(model) {
return useContext(model.getContext());
}
export default useCtxModelContext;
- 封装
CtxModel
js
import { createContext } from 'react';
const _state = Symbol('_state');
const _actions = Symbol('_actions');
const _Context = Symbol('_Context');
class CtxModel {
constructor({ state, actions }) {
this[_state] = state;
this[_actions] = actions;
this[_Context] = createContext({
// 暂时不给值 ...
});
}
getState() {
return this[_state];
}
getActions() {
return this[_actions];
}
getContext() {
return this[_Context];
}
getModelConsumer() {
return this[_Context].Consumer;
}
}
export default CtxModel;
总结:
- React 是一个非常灵活的 JavaScript 视图库,在一些特殊情况下,不一定需要引入 store、center 之类的操作概念。
- React 的跨组件共享几乎都是基于 Context API,state 可以自由发挥设计。