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 許可證)