📜 ⬆️ ⬇️

State management and efficient rendering in React applications

Hello! I want to talk about the next implementation of Flux. Or rather, about the minimum implementation that we successfully use in work projects. And how we came to this. In fact, many somehow come to this decision themselves. The solution described here is just a variation of those like it.

At Redradix, we have been developing web applications for React for about a year, and during this time, each of the team members had ideas that we gradually brought to our own home decision. We immediately abandoned repositories in classic Flux in favor of a single global state. Stores only serve as setters / getters to the application state. What is good global state? One state is one config of the whole application. It can easily be replaced by another, saved or transmitted over the network. No more dependencies between repositories.

The question arises: how to divide this state between the components in the application? The simplest and most easily implemented solution is the so-called top-down rendering. The root component subscribes to changes in the state and after each change it receives the current version of the state, which it passes further along the component tree. Thus, all components in the application have access to the state and can read the necessary data from it. This approach has two problems: rendering inefficiency (the entire component tree is updated for every change in state) and the need to explicitly transfer state to all components (state-dependent components may be inside independent components). The second problem is solved with the help of the context, to pass the state implicitly. But how to get away from updating the entire application for every sneeze?
')
Therefore, we left top-down rendering. I liked the idea of ​​Relay with collocation of requests inside the component, which needs data on these requests. Relay covers not only state management, but also work with the server. For the time being, we have only focused on managing the status of the client.

The idea is simple: to describe the requests to the global state inside the component and to sign all such components for changes in the state for the given requests. Now it turns out that the data from the state will receive only those components that really need them. And not all the component tree will be updated, but only those parts of it that are subscribed to the data being changed. Such a component looks like this:

const MyComponent = React.createClass({ statics: { queries: { count: ['ui', 'counter', 'count'] } }, render() { return <button>{this.props.count}</button>; } }); export default connect(MyComponent); 


The data from the query falls into a property with the name of the query, in this case, the property count . Signing on the change occurs inside the special function connect , in which the component is wrapped with requests.

Let's take a look inside this feature.

Code
 import React from 'react'; import equal from 'deep-equal'; import { partial } from 'fn.js'; import { is } from 'immutable'; import { getIn, addChangeListener, removeChangeListener } from './atom'; function resolveQueries(queries) { return Object.entries(queries) .reduce((resolved, [name, query]) => { resolved[name] = getIn(query); return resolved; }, {}); } function stateEqual(state, nextState) { return Object.keys(state) .every((name) => is(state[name], nextState[name])); } export default function connect(Component) { //   const queries = Component.queries; //          const getNextState = partial(resolveQueries, queries); //       let state = {}; return React.createClass({ //      displayName: `${Component.displayName}::Connected`, componentWillMount() { //   state = getNextState(); }, componentDidMount() { //       //       addChangeListener(queries, this._update); }, componentWillReceiveProps(nextProps) { //  ,    if (equal(this.props, nextProps) === false) { this.forceUpdate(); } }, shouldComponentUpdate() { //  SCU, // ..      forceUpdate return false; }, componentWillUnmount() { removeChangeListener(queries, this._update); }, _update() { const nextState = getNextState(); //          . //     . if (stateEqual(state, nextState) === false) { state = nextState; this.forceUpdate(); } }, render() { //        return <Component {...this.props} {...state} />; } }); } 



As you can see, the function above returns the React component, which controls the state and passes it to the component to be wrapped. The _update method before updating the component checks whether the data on the requests has actually changed. This is necessary for cases when a change occurs in the state tree for which a component is signed. Then, if this part has not really changed, the component will not be updated. In this example, I used the Immutable library for immutable data structures, but you can use whatever you want, it doesn't matter.

Another part of the implementation is in a module called atom . A module is an interface with getters / setters to a state object. I usually have three functions to read and write to the state: getIn , assocIn, and updateIn . These functions can be wrappers around the Immutable or mori library methods, or something else. A wrapper is needed only to replace the current state with a new one after it has been changed (you can also add logging operations).

 let state; export function getIn(query) { return state.getIn(query); } export function assocIn(query, value) { state = state.setIn(query, value); } export function updateIn(query, fn) { state = state.updateIn(query, fn); } 


We will also need a functionality for signing components on changes by requests and calling these listeners when the data on requests has been changed using the above described functions.

 const listeners = {}; export function addChangeListener(queries, fn) { Object.values(queries) .forEach((query) => { const sQuery = JSON.stringify(query); listeners[sQuery] = listeners[sQuery] || []; listeners[sQuery].push(fn); }); } 


Now state-changing functions should also report changes:

 //   export function assocIn(query, value) { swap(state.setIn(query, value), query); } //      export function swap(nextState, query) { state = nextState; notifySwap(query); } //        , //     export function notifySwap(query) { let sQuery = JSON.stringify(query); sQuery = sQuery.slice(0, sQuery.length - 1); Object.entries(listeners) .forEach(([lQuery, fns]) => { if (lQuery.startsWith(sQuery)) { fns.forEach((fn) => fn()); } }); } 


Putting all the parts together, the state change and the processing of this change in the application will be as follows:



It remains only to initialize the state. I usually do this just before initializing the component tree.

 import React from 'react'; import { render } from 'react-dom'; import Root from './components/root.jsx'; import { silentSwap } from './lib/atom'; import { fromJS } from 'immutable'; const initialState = { ui: { counter: { count: 0 } } }; silentSwap(fromJS(initialState)); render(<Root />, document.getElementById('app')); 


Here is an example of the repository, which now plays the role of setter in the state:

 import { updateIn } from '../lib/atom'; import { listen } from '../lib/dispatcher'; import actions from '../config/actions'; import { partial } from 'fn.js'; const s = { count: ['ui', 'counter', 'count'] }; listen(actions.INC_COUNT, partial(updateIn, s.count, (count) => count + 1)); listen(actions.DEC_COUNT, partial(updateIn, s.count, (count) => count - 1)); 


Returning to the problems we had with top-down rendering:



There are plans to do something with this to work with the server, or rather to get all the data in one request (as does Relay and Falcor). For example, Om Next pulls requests from all components into one data structure, calculates its hash and sends these requests to the server. Thus, for the same requests, there will always be the same hash, which means you can cache the server's response using this hash. Pretty idle idea. Look at David Nollen's report on Om Next , many cool ideas.

All the code from the article is here: gist.github.com/roman01la/912265347dd5c46b0a2a

Perhaps you use a similar solution or something better? Tell, interestingly!

Source: https://habr.com/ru/post/271819/


All Articles