unstated 是基于 Class Component 的数据流管理库,unstated-next 是针对 Function Component 的升级版,且特别优化了对 Hooks 的支持。前端
与类 redux 库相比,这个库设计的别出心裁,并且这两个库源码行数都特别少,与 180 行的 unstated 相比,unstated-next 只有不到 40 行,但想象空间却更大,且用法符合直觉,因此本周精读就会从用法与源码两个角度分析这两个库。react
首先问,什么是数据流?React 自己就提供了数据流,那就是 setState
与 useState
,数据流框架存在的意义是解决跨组件数据共享与业务模型封装。git
还有一种说法是,React 早期声称本身是 UI 框架,不关心数据,所以须要生态提供数据流插件弥补这个能力。但其实 React 提供的 createContext
与 useContext
已经能解决这个问题,只是使用起来稍显麻烦,而 unstated 系列就是为了解决这个问题。github
unstated 解决的是 Class Component 场景下组件数据共享的问题。redux
相比直接抛出用法,笔者还原一下做者的思考过程:利用原生 createContext
实现数据流须要两个 UI 组件,且实现方式冗长:api
const Amount = React.createContext(1);
class Counter extends React.Component {
state = { count: 0 };
increment = amount => {
this.setState({ count: this.state.count + amount });
};
decrement = amount => {
this.setState({ count: this.state.count - amount });
};
render() {
return (
<Amount.Consumer>
{amount => (
<div>
<span>{this.state.count}</span>
<button onClick={() => this.decrement(amount)}>-</button>
<button onClick={() => this.increment(amount)}>+</button>
</div>
)}
</Amount.Consumer>
);
}
}
class AmountAdjuster extends React.Component {
state = { amount: 0 };
handleChange = event => {
this.setState({
amount: parseInt(event.currentTarget.value, 10)
});
};
render() {
return (
<Amount.Provider value={this.state.amount}>
<div>
{this.props.children}
<input
type="number"
value={this.state.amount}
onChange={this.handleChange}
/>
</div>
</Amount.Provider>
);
}
}
render(
<AmountAdjuster>
<Counter />
</AmountAdjuster>
);
复制代码
而咱们要作的,是将 setState
从具体的某个 UI 组件上剥离,造成一个数据对象实体,能够被注入到任何组件。promise
这就是 unstated
的使用方式:微信
import React from "react";
import { render } from "react-dom";
import { Provider, Subscribe, Container } from "unstated";
class CounterContainer extends Container {
state = {
count: 0
};
increment() {
this.setState({ count: this.state.count + 1 });
}
decrement() {
this.setState({ count: this.state.count - 1 });
}
}
function Counter() {
return (
<Subscribe to={[CounterContainer]}> {counter => ( <div> <button onClick={() => counter.decrement()}>-</button> <span>{counter.state.count}</span> <button onClick={() => counter.increment()}>+</button> </div> )} </Subscribe>
);
}
render(
<Provider> <Counter /> </Provider>,
document.getElementById("root")
);
复制代码
首先要为 Provider
正名:Provider
是解决单例 Store 的最佳方案,当项目与组件都是用了数据流,须要分离做用域时,Provider
便派上了用场。若是项目仅需单 Store 数据流,那么与根节点放一个 Provider
等价。app
其次 CounterContainer
成为一个真正数据处理类,只负责存储与操做数据,经过 <Subscribe to={[CounterContainer]}>
RenderProps 方法将 counter
注入到 Render 函数中。框架
unstated 方案本质上利用了 setState
,但将 setState
与 UI 剥离,并能够很方便的注入到任何组件中。
相似的是,其升级版 unstated-next
本质上利用了 useState
,利用了自定义 Hooks 能够与 UI 分离的特性,加上 useContext
的便捷性,利用不到 40 行代码实现了比 unstated
更强大的功能。
unstated-next
用 40 行代码号称 React 数据管理库的终结版,让咱们看看它是怎么作到的!
仍是从思考过程提及,笔者发现其 README 也提供了对应思考过程,就以其 README 里的代码做为案例。
首先,使用 Function Component 的你会这样使用数据流:
function CounterDisplay() {
let [count, setCount] = useState(0);
let decrement = () => setCount(count - 1);
let increment = () => setCount(count + 1);
return (
<div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div>
);
}
复制代码
若是想将数据与 UI 分离,利用 Custom Hooks 就能够完成,这不须要借助任何框架:
function useCounter() {
let [count, setCount] = useState(0);
let decrement = () => setCount(count - 1);
let increment = () => setCount(count + 1);
return { count, decrement, increment };
}
function CounterDisplay() {
let counter = useCounter();
return (
<div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
);
}
复制代码
若是想将这个数据分享给其余组件,利用 useContext
就能够完成,这不须要借助任何框架:
function useCounter() {
let [count, setCount] = useState(0);
let decrement = () => setCount(count - 1);
let increment = () => setCount(count + 1);
return { count, decrement, increment };
}
let Counter = createContext(null);
function CounterDisplay() {
let counter = useContext(Counter);
return (
<div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
);
}
function App() {
let counter = useCounter();
return (
<Counter.Provider value={counter}> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ); } 复制代码
但这样仍是显示使用了 useContext
的 API,而且对 Provider
的封装没有造成固定模式,这就是 usestated-next
要解决的问题。
因此这就是 unstated-next
的使用方式:
import { createContainer } from "unstated-next";
function useCounter() {
let [count, setCount] = useState(0);
let decrement = () => setCount(count - 1);
let increment = () => setCount(count + 1);
return { count, decrement, increment };
}
let Counter = createContainer(useCounter);
function CounterDisplay() {
let counter = Counter.useContainer();
return (
<div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
);
}
function App() {
return (
<Counter.Provider> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ); } 复制代码
能够看到,createContainer
能够将任何 Hooks 包装成一个数据对象,这个对象有 Provider
与 useContainer
两个 API,其中 Provider
用于对某个做用域注入数据,而 useContainer
能够取到这个数据对象在当前做用域的实例。
对 Hooks 的参数也进行了规范化,咱们能够经过 initialState
设定初始化数据,且不一样做用域能够嵌套并赋予不一样的初始化值:
function useCounter(initialState = 0) {
let [count, setCount] = useState(initialState);
let decrement = () => setCount(count - 1);
let increment = () => setCount(count + 1);
return { count, decrement, increment };
}
const Counter = createContainer(useCounter);
function CounterDisplay() {
let counter = Counter.useContainer();
return (
<div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</span> <button onClick={counter.increment}>+</button> </div>
);
}
function App() {
return (
<Counter.Provider>
<CounterDisplay />
<Counter.Provider initialState={2}>
<div>
<div>
<CounterDisplay />
</div>
</div>
</Counter.Provider>
</Counter.Provider>
);
}
复制代码
能够看到,React Hooks 已经很是适合作状态管理,而生态应该作的事情是尽量利用其能力进行模式化封装。
有人可能会问,取数和反作用怎么办?
redux-saga
和其余中间件都没有,这个数据流是否是阉割版?
首先咱们看 Redux 为何须要处理反作用的中间件。这是由于 reducer
是一个同步纯函数,其返回值就是操做结果中间不能有异步,且不能有反作用,因此咱们须要一种异步调用 dispatch
的方法,或者一个反作用函数来存放这些 “脏” 逻辑。
而在 Hooks 中,咱们能够随时调用 useState
提供的 setter
函数修改值,这早已自然解决了 reducer
没法异步的问题,同时也实现了 redux-chunk
的功能。
而异步功能也被 useEffect
这个 React 官方 Hook 替代。咱们看到这个方案能够利用 React 官方提供的能力彻底覆盖 Redux 中间件的能力,对 Redux 库实现了降维打击,因此下一代数据流方案随着 Hooks 的实现是真的存在的。
最后,相比 Redux 自身以及其生态库的理解成本(笔者不才,初学 Redux 以及其周边 middleware 时理解了很久),Hooks 的理解学习成本明显更小。
不少时候,人们排斥一个新技术,并非由于新技术很差,而是这可能让本身多年精通的老手艺带来的 “竞争优点” 彻底消失。可能一个织布老专家手工织布效率是入门学员的 5 倍,但换上织布机器后,这个差别很快会被抹平,老织布专家面临被淘汰的危机,因此维护这份老手艺就是维护他本身的利益。但愿每一个团队中的老织布工人都能主动引入织布机。
再看取数中间件,咱们通常须要解决 取数业务逻辑封装 与 取数状态封装,经过 redux 中间件能够封装在内,经过一个
dispatch
解决。
其实 Hooks 思惟下,利用 swr useSWR
同样能解决:
function Profile() {
const { data, error } = useSWR("/api/user");
}
复制代码
取数的业务逻辑封装在 fetcher
中,这个在 SWRConfigContext.Provider
时就已注入,还能够控制做用域!彻底利用 React 提供的 Context 能力,能够感觉到实现底层原理的一致性和简洁性,越简单越优美的数学公式越多是真理。
而取数状态已经封装在 useSWR
中,配合 Suspense 能力,连 Loading 状态都不用关心了。
咱们再梳理一下 unstated
这个库作了哪些事情。
Provider
申明做用范围。Container
做为能够被继承的类,继承它的 Class 做为 Store。Subscribe
做为 RenderProps 用法注入 Store,注入的 Store 实例由参数 to
接收到的 Class 实例决定。对于第一点,Provider
在 Class Component 环境下要初始化 StateContext
,这样才能在 Subscribe
中使用:
const StateContext = createReactContext(null);
export function Provider(props) {
return (
<StateContext.Consumer>
{parentMap => {
let childMap = new Map(parentMap);
if (props.inject) {
props.inject.forEach(instance => {
childMap.set(instance.constructor, instance);
});
}
return (
<StateContext.Provider value={childMap}>
{props.children}
</StateContext.Provider>
);
}}
</StateContext.Consumer>
);
}
复制代码
对于第二点,对于 Container
,须要提供给 Store setState
API,按照 React 的 setState
结构实现了一遍。
值得注意的是,还存储了一个 _listeners
对象,而且可经过 subscribe
与 unsubscribe
增删。
_listeners
存储的实际上是当前绑定的组件 onUpdate
生命周期,而后在 setState
时主动触发对应组件的渲染。onUpdate
生命周期由 Subscribe
函数提供,最终调用的是 this.setState
,这个在 Subscribe
部分再说明。
如下是 Container
的代码实现:
export class Container<State: {}> {
state: State;
_listeners: Array<Listener> = [];
constructor() {
CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this));
}
setState(
updater: $Shape<State> | ((prevState: $Shape<State>) => $Shape<State>),
callback?: () => void
): Promise<void> {
return Promise.resolve().then(() => {
let nextState;
if (typeof updater === "function") {
nextState = updater(this.state);
} else {
nextState = updater;
}
if (nextState == null) {
if (callback) callback();
return;
}
this.state = Object.assign({}, this.state, nextState);
let promises = this._listeners.map(listener => listener());
return Promise.all(promises).then(() => {
if (callback) {
return callback();
}
});
});
}
subscribe(fn: Listener) {
this._listeners.push(fn);
}
unsubscribe(fn: Listener) {
this._listeners = this._listeners.filter(f => f !== fn);
}
}
复制代码
对于第三点,Subscribe
的 render
函数将 this.props.children
做为一个函数执行,并把对应的 Store 实例做为参数传递,这经过 _createInstances
函数实现。
_createInstances
利用 instanceof
经过 Class 类找到对应的实例,并经过 subscribe
将本身组件的 onUpdate
函数传递给对应 Store 的 _listeners
,在解除绑定时调用 unsubscribe
解绑,防止没必要要的 renrender。
如下是 Subscribe
源码:
export class Subscribe<Containers: ContainersType> extends React.Component<
SubscribeProps<Containers>,
SubscribeState
> {
state = {};
instances: Array<ContainerType> = [];
unmounted = false;
componentWillUnmount() {
this.unmounted = true;
this._unsubscribe();
}
_unsubscribe() {
this.instances.forEach(container => {
container.unsubscribe(this.onUpdate);
});
}
onUpdate: Listener = () => {
return new Promise(resolve => {
if (!this.unmounted) {
this.setState(DUMMY_STATE, resolve);
} else {
resolve();
}
});
};
_createInstances(
map: ContainerMapType | null,
containers: ContainersType
): Array<ContainerType> {
this._unsubscribe();
if (map === null) {
throw new Error(
"You must wrap your <Subscribe> components with a <Provider>"
);
}
let safeMap = map;
let instances = containers.map(ContainerItem => {
let instance;
if (
typeof ContainerItem === "object" &&
ContainerItem instanceof Container
) {
instance = ContainerItem;
} else {
instance = safeMap.get(ContainerItem);
if (!instance) {
instance = new ContainerItem();
safeMap.set(ContainerItem, instance);
}
}
instance.unsubscribe(this.onUpdate);
instance.subscribe(this.onUpdate);
return instance;
});
this.instances = instances;
return instances;
}
render() {
return (
<StateContext.Consumer>
{map =>
this.props.children.apply(
null,
this._createInstances(map, this.props.to)
)
}
</StateContext.Consumer>
);
}
}
复制代码
总结下来,unstated
将 State 外置是经过自定义 Listener 实现的,在 Store setState
时触发收集好的 Subscribe
组件的 rerender。
unstated-next
这个库只作了一件事情:
createContainer
将自定义 Hooks 封装为一个数据对象,提供 Provider
注入与 useContainer
获取 Store 这两个方法。正如以前解析所说,unstated-next
可谓将 Hooks 用到了极致,认为 Hooks 已经彻底具有数据流管理的所有能力,咱们只要包装一层规范便可:
export function createContainer(useHook) {
let Context = React.createContext(null);
function Provider(props) {
let value = useHook(props.initialState);
return <Context.Provider value={value}>{props.children}</Context.Provider>;
}
function useContainer() {
let value = React.useContext(Context);
if (value === null) {
throw new Error("Component must be wrapped with <Container.Provider>");
}
return value;
}
return { Provider, useContainer };
}
复制代码
可见,Provider
就是对 value
进行了约束,固化了 Hooks 返回的 value 直接做为 value
传递给 Context.Provider
这个规范。
而 useContainer
就是对 React.useContext(Context)
的封装。
真的没有其余逻辑了。
惟一须要思考的是,在自定义 Hooks 中,咱们用 useState
管理数据仍是 useReducer
管理数据的问题,这个是个仁者见仁的问题。不过咱们能够对自定义 Hooks 进行嵌套封装,支持一些更复杂的数据场景,好比:
function useCounter(initialState = 0) {
const [count, setCount] = useState(initialState);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return { count, decrement, increment };
}
function useUser(initialState = {}) {
const [name, setName] = useState(initialState.name);
const [age, setAge] = useState(initialState.age);
const registerUser = userInfo => {
setName(userInfo.name);
setAge(userInfo.age);
};
return { user: { name, age }, registerUser };
}
function useApp(initialState) {
const { count, decrement, increment } = useCounter(initialState.count);
const { user, registerUser } = useUser(initialState.user);
return { count, decrement, increment, user, registerUser };
}
const App = createContainer(useApp);
复制代码
借用 unstated-next
的标语:“never think about React state management libraries ever again” - 用了 unstated-next
不再要考虑其余 React 状态管理库了。
而有意思的是,unstated-next
自己也只是对 Hooks 的一种模式化封装,Hooks 已经能很好解决状态管理的问题,咱们真的不须要 “再造” React 数据流工具了。
讨论地址是:精读《unstated 与 unstated-next 源码》 · Issue #218 · dt-fe/weekly
若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)