1 Introduction
unstated Class Component-based data flow management database, unstated-next It is an upgraded version of Function Component with special optimization for Hooks support.
Compared with redux-like libraries, this library is designed to be unique, and the source lines of both libraries are exceptionally small. Untated-next has less than 40 lines compared to 180 lines of unstated, but has more imagination and intuitive usage, so this week's intensive reading will analyze both libraries from the usage and source points of view.
2 Overview
First of all, what is data flow?React itself provides data streams, setState and useState, and the meaning of the data flow framework is to solve cross-component data sharing and business model encapsulation.
Another argument is that React claims to be a UI framework in the early days and does not care about data, so it needs to be complemented by an ecological data flow plug-in.However, the createContext and useContext provided by React can solve this problem, but they are slightly more cumbersome to use, and the unstated series is to solve this problem.
unstated
Untated solves the problem of component data sharing in a Class Component scenario.
In contrast to the direct throw usage, the author restores the author's thinking process that using native createContext to implement data flow requires two UI components and is implemented in a lengthy manner:
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> );
What we need to do is strip setState from a specific UI component to form a data object entity that can be injected into any component.
This is how unstated is used:
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") );
First, correct the name of the Provider: Provider is the best solution for a single Store. Provider is useful when both projects and components use data streams and need to be scoped apart.If the project only requires a single Store stream, then it is equivalent to placing a Provider at the root node.
Then CounterContainer becomes a real data processing class, responsible only for storing and manipulating data, and counter is injected into the Render function through the <Subscribe to={[CounterContainer]}> RenderProps method.
The unstated scheme essentially takes advantage of setState, but it strips the setState from the UI and can easily be injected into any component.
Similarly, its upgraded version of unstated-next essentially leverages useState, takes advantage of the separation of custom Hooks from the UI, and the convenience of useContext, which allows less than 40 lines of code to perform more powerful functions than unstated.
unstated-next
Untated-next is the final version of the React Data Management Library in 40 lines of code. Let's see how it works!
Or starting from the process of thinking, I find that its README also provides a corresponding process of thinking, using the code in its README as a case.
First, with Function Component, you would use data streams like this:
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> ); }
If you want to separate the data from the UI, you can do this with Custom Hooks, which does not require any framework:
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> ); }
If you want to share this data with other components, you can do it with useContext, without any framework:
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> ); }
However, it still shows the API using useContext, and there is no fixed pattern for encapsulating Provider, which is the problem usestated-next has to solve.
So this is how unstated-next is used:
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> ); }
You can see that createContainer can wrap any Hooks into a data object that has two API s, Provider and useContainer, where Provider is used to inject data into a scope and useContainer can fetch an instance of the data object in the current scope.
Hooks parameters are also normalized, and initialization data can be set through the initial state, and different scopes can be nested and assigned different initialization values:
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> ); }
As you can see, React Hooks are already well suited for state management, and what an ecology should do is to use its capabilities as much as possible for modal encapsulation.
One might ask, what about counts and side effects?No redux-saga or other middleware, is this data stream castrated?
First let's look at why Redux needs middleware to handle side effects.This is because reducer is a synchronous pure function whose return value is that there can be no asynchronous and no side effects in the result of the operation, so we need a method to call dispatch asynchronously or a side effect function to store these "dirty" logic.
In Hooks, we can call the setter function provided by useState to modify the value at any time, which has naturally solved the problem that reducer cannot be asynchronous, and also implemented the redux-chunk function.
Asynchronous functionality has also been replaced by useEffect, the React's official Hook.We see that this scheme can take advantage of the capabilities officially provided by React to completely override the Redux middleware and hit the Redux library with dimensionality reduction, so the next generation of streaming schemes really exists with the implementation of Hooks.
Finally, Hooks'cost of understanding learning is significantly lower than that of Redux itself and its ecosystem (I'm not a talented writer, I've understood Redux for a long time at first and middleware around it).
Many times, people reject a new technology, not because it is not good, but because it may completely eliminate the "competitive advantage" brought by years of old craftsmanship.Perhaps an old knitting expert knits cloth by hand five times as efficiently as a beginner, but after replacing the knitting machine, the difference will be smoothed out soon. The old knitting expert is facing the crisis of being eliminated, so maintaining this old knitting is to safeguard his own interests.We hope that the old weavers in each team can actively introduce the looms.
Looking at number middleware, we usually need to solve the business logic encapsulation and number state encapsulation of number, which can be encapsulated by redux middleware and solved by dispatch.
In fact, with Hooks thinking, use swr >) UseSWR solves the same problem:
function Profile() { const { data, error } = useSWR("/api/user"); }
Number business logic is encapsulated in fetcher, which is injected at SWRConfigContext.Provider and can also control scope!Fully utilizing the Context capabilities provided by React, you can feel the consistency and simplicity of the underlying principles, and the simpler, the more beautiful the mathematical formulas are, the more likely they are to be true.
The counting state is encapsulated in useSWR, and with Suspense capabilities, even the Loading state is not of concern.
3 Intensive reading
unstated
Let's go over what the unstated library does.
- Use Provider to declare scope.
- Provide Container as a class that can be inherited and Class that inherits it as a Store.
- Provide Subscribe to inject into Store as a RenderProps usage, and the injected Store instance is determined by the Class instance received by the parameter to.
For the first point, the Provider initializes the StateContext in the Class Component environment so that it can be used in 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> ); }
For the second point, for Container, you need to provide the Store setState API, which is implemented once according to the setState structure of React.
It is worth noting that a _listeners object is also stored and can be added or deleted by subscribe and unsubscribe.
_listeners stores the onUpdate lifecycle of the currently bound component and then actively triggers rendering of the corresponding component when setState is set.The onUpdate life cycle is provided by the Subscribe function, which ultimately calls this.setState, which is explained in the Subscribe section.
The following is the code implementation for 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); } }
For the third point, Subscribe's render function executes this.props.children as a function and passes the corresponding Store instance as a parameter, which is implemented through the _createInstances function.
_createInstances uses instanceof to find the corresponding instance through the Class class, passes the onUpdate function of its component to the _listeners of the corresponding Store through subscribe, and calls unsubscribe to unbind when unbound to prevent unnecessary renrender.
The following is the Subscribe source:
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> ); } }
In summary, unstated externalizes the State by customizing the Listener to trigger the rerender of the collected Subscribe component when Store setState.
unstated-next
The unstated-next library does only one thing:
- Provide createContainer to encapsulate custom Hooks as a data object, and provide two methods, Provider injection and useContainer get Store.
As previously explained, unstated-next is the ultimate use of Hooks, assuming that Hooks already has all the capabilities to manage data streams, and that all we need to do is wrap up a layer of specifications:
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 }; }
Thus, a Provider constrains a value, solidifying the specification that Hooks returns by passing the value directly to Context.Provider as a value.
The useContainer is the encapsulation of React.useContext.
There really is no other logic.
The only thing to think about is whether we use useState to manage data or useReducer to manage data in custom Hooks, which is a matter of personal opinion.However, we can nest and encapsulate custom Hooks to support more complex data scenarios, such as:
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);
4 Summary
Borrow the slogan "never think about React state management libraries ever again" - use unstated-next and never think about other React state management libraries anymore.
Interestingly, unstated-next itself is just a schematic encapsulation of Hooks, which already solves the state management problem very well and we really don't need to "re-create" the React data flow tools.
The discussion address is: Read "Untated and unstated-next Source". Issue #218. dt-fe/weekly
If you want to participate in the discussion, please click here , with new themes every week, released on weekends or Mondays.Front End Intensive Reading - helps you filter the contents of your profile.
Focus on front-end reading WeChat Public Number
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
Copyright Notice: Free Reproduction - Non-Commercial - Non-Derived - Preserve Signature ( Creative Sharing 3.0 License)