1 the introduction
Unstated is a data flow management library based on Class Component, and unstated-Next is an updated version of Function Component with optimized support for Hooks.
Compared with the redux-like library, the design of this library is very innovative, and the number of source lines of both libraries is very small. Compared with the number of unstated lines of 180, unstated-next only has less than 40 lines, but it has more imagination and intuitive usage. So this week’s intensive reading will examine both libraries from both usage and source perspectives.
2 an overview
First of all, what is data flow? React itself provides data flow, which issetState
与 useState
Data flow frameworks exist to address cross-component data sharing and business model encapsulation.
Another argument is that React early on claimed to be a UI framework that didn’t care about data, and therefore needed an ecosystem to provide data flow plugins to compensate for this capability. In fact, React provides createContext and useContext to solve this problem, but it is a bit troublesome to use. The Unstated series aims to solve this problem.
unstated
Unstated addresses the problem of Component data sharing in the Class Component scenario.
Instead of just throwing it, I’m going to restate the author’s thought process: Using native createContext to stream data requires two UI components, and the implementation is tedious:
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>
);
Copy the code
What we want to do is strip setState from a specific UI component into a data object entity that can be injected into any component.
This is an unstated use:
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"));Copy the code
The first step is to justify providers: Providers are the best solution for singleton stores, and they come in handy when projects and components both use data flows and need to be scoped separately. If the project requires only a single Store data stream, then it is equivalent to having a Provider on the root node.
Second, CounterContainer becomes a true data-handling class that only stores and manipulates data, Inject counter into the Render function with
RenderProps.
The unstated proposal essentially exploitedsetState
But will besetState
Decouple from the UI and can be easily injected into any component.
Similarly, an updated version of Unststation-Next essentially utilizes useState, taking advantage of the fact that custom Hooks can be isolated from the UI, and the convenience of useContext, achieving more power than Unststation-next in less than 40 lines of code.
unstated-next
Unstated – Next claims to be the definitive version of the React data management library with 40 lines of code, so let’s see how it does it!
Starting with the thought process, the author found that its README also provides corresponding thought process, and took the code in its README as an example.
First, if you’re using Function Component, you’ll use the data stream like this:
function CounterDisplay() {
let [count, setCount] = useState(0);
let decrement = (a)= > setCount(count - 1);
let increment = (a)= > setCount(count + 1);
return (
<div>
<button onClick={decrement}>-</button>
<p>You clicked {count} times</p>
<button onClick={increment}>+</button>
</div>
);
}
Copy the code
If you want to separate data from the UI, you can do so using Custom Hooks, without using any framework:
function useCounter() {
let [count, setCount] = useState(0);
let decrement = (a)= > setCount(count - 1);
let increment = (a)= > 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>
);
}
Copy the code
If you want to share this data with other components, you can do so using useContext, without using any framework:
function useCounter() {
let [count, setCount] = useState(0);
let decrement = (a)= > setCount(count - 1);
let increment = (a)= > 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>
);
}
Copy the code
However, this still shows the use of the useContext API, and there is no fixed pattern in the encapsulation of the Provider, which is the problem usestated-Next addresses.
So that’s how unstated-next is used:
import { createContainer } from "unstated-next";
function useCounter() {
let [count, setCount] = useState(0);
let decrement = (a)= > setCount(count - 1);
let increment = (a)= > 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>
);
}
Copy the code
As you can see, createContainer can wrap any Hooks into a data object that has a Provider and useContainer API, where Provider is used to inject data into a scope, UseContainer can take an instance of this data object in the current scope.
InitialState is used to set initialization data, and different scopes can be nested and assigned different initialization values:
function useCounter(initialState = 0) {
let [count, setCount] = useState(initialState);
let decrement = (a)= > setCount(count - 1);
let increment = (a)= > 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> ); }Copy the code
As you can see, React Hooks are already well suited for state management, and the ecology should be doing what it can to model encapsulation as much as possible.
One might ask, what about taking numbers and side effects? Neither Redux-Saga nor any other middleware has this data stream. Is it a castrated version?
First let’s look at why Redux needs middleware to handle side effects. This is because Reducer is a synchronous pure function, and its return value is that operation results cannot be asynchronous or have side effects, 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 values at any time, which has naturally solved the problem that reducer cannot be asynchronous, and also realized the function of Redux-chunk.
The asynchronous function is also replaced by useEffect, the React official Hook. We can see that this solution can take advantage of the capabilities provided by React to fully override the Redux middleware capabilities, achieving dimension reduction against the Redux library, so the next generation data flow solution is real with Hooks implementation.
Finally, Hooks cost significantly less to understand than Redux itself and its ecosystem (which I unfortunately understood for a long time when I was a beginner of Redux and the middleware around it).
Many times, people reject a new technology not because it’s bad, but because it can completely eliminate the “competitive advantage” of an old craft they’ve mastered for years. It is possible that the efficiency of an old weaving expert is 5 times that of an entry-level student, but after changing into a weaving machine, the difference will be erased quickly. The old weaving expert is facing the crisis of being eliminated, so maintaining this old craft is to safeguard his own interests. It is hoped that the old weavers in each team will take the initiative to introduce the loom.
As for the fetch middleware, we generally need to solve the package of fetch business logic and fetch state. Redux middleware can be encapsulated inside and solve the problem through a dispatch.
UseSWR useSWR to solve this problem.
function Profile() {
const { data, error } = useSWR("/api/user");
}
Copy the code
Access business logic encapsulation in fetcher, this in SWRConfigContext. Had been injected, the Provider can control scope! By fully utilizing the Context capability provided by React, you can feel the consistency and simplicity of the underlying principles. The simpler and more elegant the mathematical formula, the more likely it is to be true.
While the fetch state has been encapsulated in useSWR, with Suspense ability, you don’t even need to care about the Loading state.
3 intensive reading
unstated
Again, let’s sort out what unstated library does.
- using
Provider
State scope. - provide
Container
As a Class that can be inherited, inherit its Class as a Store. - provide
Subscribe
Injected into the Store as RenderProps, the injected Store instance is provided by the parametersto
Received Class instance decision.
For the first point, the Provider initializes StateContext in the Class Component environment to use it 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>
);
}
Copy the code
For Container, the Store setState API needs to be provided. The setState structure of React is implemented.
Note that a _Listeners object is also stored, and can be added or deleted via SUBSCRIBE and unsubscribe.
The _Listeners store the onUpdate lifecycle of the currently bound component, and then actively trigger the rendering of the corresponding component on setState. The onUpdate lifecycle is provided by the Subscribe function, which ultimately calls this.setState, as explained in the Subscribe section.
Here is the code implementation of the 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);
}
}
Copy the code
For the third point, the render function of Subscribe executes this.props. Children as a function and passes the corresponding Store instance as an argument, which is implemented by the _createInstances function.
Createinstances find instances of the instanceof Class and subscribe to the _listeners of the Store. Call unsubscribe unbinding on unbinding to prevent unnecessary renrender.
Here 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>
);
}
}
Copy the code
To sum up, the unstated externalization of State was achieved through a custom Listener, which triggered the rerender of the collected Subscribe component when Store setState.
unstated-next
The unstated-next library does only one thing:
- provide
createContainer
Encapsulate custom Hooks as a data object, providedProvider
Injection anduseContainer
Get both methods of Store.
Unststation-next takes the Hooks to the extreme and says that Hooks are fully capable of data flow management, we just need to wrap a layer of specification:
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 };
}
Copy the code
Provider is a constraint on values, which are returned by Hooks and passed directly to Context.Provider as values.
UseContainer is a wrapper around React. UseContext (Context).
There really is no other logic.
The only thing to think about is whether we use useState or useReducer to manage data in custom Hooks, which is an open question. 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 = (a)= > setCount(count - 1);
const increment = (a)= > 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);
Copy the code
4 summarizes
To borrow the slogan of Unstated-Next: “Never think about React state management libraries ever again” – unstated- Next Do not consider any other React state management libraries.
Interestingly, unststation-next itself is just a stylized encapsulation of Hooks, which already do a good job of state management, and we really don’t need to “reinvent” the React data flow tool.
Close reading of “Unstated and Unstated – Next source code” · Issue #218 · dT-fe /weekly
If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.
Pay attention to the front end of intensive reading wechat public account
Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)