📜 ⬆️ ⬇️

Restate - or how to turn a Redux log into a tree

The history of IT development is much more interesting than any soap opera, but we will not retell it. We can only say that we have witnessed the principle of "data-driven", adrenals with two-way-binding and boundless people without principles and concepts.
God made people strong and weak. Samuel Colt made them equal.
Flux and Redux did about the same thing.

There was only one problem - Redux is essentially a very primitive crap, and in order to work with it at least, you had to add a couple of middleware - thunk, saga, observable, and so on.

But this article comes from the other side, seriously asking about the composition, and, in particular, about the component model. Which is in the editor - no.



For a better understanding of the problem - let's start with the basics, and make TODO.
')

React option


Everyone loves reaction for the component model, and generally because it is very “composable”. You can take almost any component, wrap it in 10 others and it will work as you need. (remember this phrase)

//   TODO const TODOs = [todo1, todo2, todo3]; //     const Application = <TodoList todos={TODOs} /> //  TodoList,      const TodoList = ({todos}) => ( <ol> {todos.map( todo => <Todo key={todo.id} {...todo} /> </ol> ); //    TODO   const Todo = (props) => <div>......</div> 

All is well, but the question arises of what to do with events, how to control everything in general.

Simple redux option


Redux solves a lot of problems, and we will not talk about them now. I hope the code below is completely understandable and obvious to everyone.

 //   const store = createStore({ todos: [todo1, todo2, todo3] }); //   const Application = <Provider store={store}> <ConnectedTodoList/> </Provider> //    .    TODO const ConnectedTodoList = connect( state => ({todos: state.todos}), { onTodoClick } )(TodoList) //     const TodoList = ({todos}) => ( <ol> {todos.map( todo => <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> </ol> ); .... 

This is an example of a bad redux application, and this is what the original example from the redux repository looks like.

There are two problems here:
- mix React and Redux
- when any Todo changes, a rerenter of everything happens - both the Todo list and each Todo.

More correct redux


The correct option does not differ from the wrong one.

 //    .    TODO const ConnectedTodoList = connect( state => ({todos: getOnlyIds(state.todos)}), <-----    { onTodoClick } )(TodoList) const TodoList = ({todos}) => ( <ol> {todos.map( id => <ConnectedTodo key={id} id={id} /> <-----   </ol> ); //  Todo   const ConnectedTodo = connect( (state,props) => ({...state.todos[props.id])}), <-----    { onTodoClick: () => dispatch => dispatch(onTodoClick(props.id)) } )(Todo) .... 

What is better? TodoList does not depend on the contents of a particular Todo, responding only to the fact of presence. Well, Todo - he goes behind the data himself, and he adds his own Id in onTodoClick.

In essence, it is no longer possible to add “more” redaks. At the same time, React is no longer used to pass data from parents to children. As a result, no composition is possible anymore.

What is the composition?


A good example of a “problem” is an example of a Tree-view , again from the official repository.

Technically, the same TODO-list, only “nesting” appears. There is no “nesting” there, because the “stor” is completely flat, and any deepest node can be addressed according to Id. And the problem is not in this data structure, but in the fact that it is impossible to use another one!

That's because Node, when encountering child Node to render, renders ConnectedNode, and in case of redux, all Connected components are equal and connect directly to the server.
And, if the components are equal, they must also perform a completely identical action — take the element of the array with the known Id.

In other words - Components work regardless of their position in the tree, and it is absolutely impossible to take a component, wrap it back in 10, and make it work as you need - it will always work the same way. Predictable - of course, yes. Did we just want it?

Trite - try to remake any TODO list into a list of lists, ala Trello - you will have to rewrite just about everything, and certainly each connect.

And if the problem is precisely in the complete ignoring of the component's nesting of components, perhaps this is what needs to be fixed. In the sense that

Enough tolerating this!


Imagine a situation where the parent component can prepare data for your child, as he considers it necessary. Imagine a situation where a parent can control all the actions that his child performs. And remember, this is roughly what redux tried to “fix”, and it can’t be washed back.


That's just really a bit different things. The problem was the bi-directionality of the events, which spawned cascade updates that were almost impossible to control.
Plus - 99% of the examples continue to use the standard approach of Reacta, which just allows truthfully or not to throw some secret Id into the child, so that he can then read the data from the store.

Simply this can be done a little more easier, and more correctly.

Redux-restate


Redux-restate ( github ) is a miniature (50 lines *) library that accepts one or more input entries and performs certain operations on them and outputs an exit exit.
Immediately it is necessary to clarify - the number of pages does not change! restate is a view in the database, or transformation in mobx, or lens, or mapStateToProps (only ToState). This is not a “Stor.”

In fact, restate are three separate packages:
- redux-restate - lower level, do what you want
- react-redux-restate - piping around the reactor - get the story from the context, and put it back (narrow moment)
- react-redux-focus is a simplified version of restate. For use in 99% of cases.

Everything works simply:
- When “immersing” (creating a state), you can programmatically change the data that will be available to children
- During the “ascent” (event processing), you can supplement the event with the necessary data in order to correctly form the final command.

“Immersion” is simply a unidirectional action that takes the state to the input and gives the state to the exit. It is very important to memorize the intermediate result with the same recompose, just as it is done in mapStateToProps.

 const composeState = (state, props) => ({ ...state, part: recompose(operationOn(state)) }); 

A “ascent” is a unidirectional action that is performed when the subordinate component calls dispatch. Its meaning is to add that “specificity” that was removed at the moment of the creation of the state.

  const routeDispatch = (dispatch, event, props) => dispatch({...event, somethingFrom: props.data }); 

An example of "even more correct" redux:

 //    .    TODO const ConnectedTodoList = connect( state => ({todos: getOnlyIds(state.todos)}), { onTodoClick } )(TodoList) const TodoList = ({todos}) => ( <ol> {todos.map( id => <RestatedTodo key={id} id={id} /> <-----    </ol> ); //  Todo   (reactReduxFocus -    API   ) const RestatedTodo = reactReduxFocus( (state, props) => ({todo: state.todos[props.id]}), //    __ todo (dispatch, event, props) => dispatch({...event, id: props.id}); //    todoID )(ConnectedTodo) //  Todo   const ConnectedTodo = connect( (state,props) => ({...state.todo}), <-----    { onTodoClick } <----    )(Todo) .... 

As a side effect, we get the automatic areStatesEqual - if the calculated state is shallowEqual to the old value - the propagation of the change stops, which allows you to isolate different parts of the application from each other.

An example of a tree can now write “more” correctly

  const RestatedNode = reactReduxFocus( (state, props) => ({ ...state, //  node   node: state.node.children[props.nodeId] }), (dispatch, event, props) => dispatch({ ...event, //   nodeId     nodeId:[props.nodeId, ...event.nodeId] }); )(ConnectedNode); const ConnectedNode = connect( state => { ...state, children: state.node.children, leafs: state.node.leafs }........); )(Node) 

And that's all - put RestatedNode into each other - reactReduxFocus will sort out everything neatly at the beginning, and then collect all the bread crumbs back into the command line.

The question remains how to return to normal, when the "synthetic", "in which there is nothing" is no longer needed. Well, you need reactReduxRestate, which takes more than one entry per input.

  import {createProvider} from 'react-redux' import reactReduxFocus from 'react-redux-focus' import reactReduxRestate from 'react-redux-restate' const Provider = createProvider('realStore'); // restate    'realStore',   -  'store',    connect const FocusedComponent = reactReduxFocus(..., ..., { storageKey: 'realStore' })(SomeComponent); const RestatedComponent = reactReduxRestate({ //     default  ('store')  'realStore' //          ,   .   -  ? realStore: 'realStore' },..., ...)(AnotherComponent) <Provider store={realStore}> <FocusedComponent > <RestatedComponent /> -     "" <FocusedComponent /> -    ,   ,     </FocusedComponent </Provider> 

In general, the source code of all three components actually takes a couple of dozen lines, but opens up so many options for ... composites.

In principle, this is exactly what was required.

And what, so it was possible?


Restate is far from being a pioneer in this matter. For example, electron-redux is engaged in “almost” which is the same - it connects two lines (main and render), one of which is not real, and even dispatch routing from one to another.

Technically, the same effect can be achieved with the use of the restate, and with fewer crutches, but the restate has absolutely no IPC.

Or take the "system of options" Yandex. Map. It is generally not very well known (publicly and documented), but anyone who has ever used the Yandex Maps API can notice how beautifully the options “cascade” there - there is a polyline color specified at the map level, the option is called geoObjectStrokeColor, and if at the geoobject level - strokeColor.

In general, “prefixing” works, and it can be found everywhere — the best example of work — in the map setup example, “scrollZoomSpeed” is set, the control component itself will read from the store simply “speed”.

Only in Yandex.Maps this prefixing works from bottom to top - the prefixes are added from the child to the parent until a match is found, which almost excludes the possibility of stopping updates, changing what value means you cannot find out which component will read this value in a cunning way.

Restate is an attempt to cross exactly Yandex.Maps and redux. Fix both. Come on and take long vacation nights.

-> github.com/thekashey/restate

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


All Articles