getDerivedStateFromState - or how to make a complex

I love React. I love for the way he works. For the fact that he does things "right". HOC, Composition, RenderProps, Stateless, Stateful - a million patrons and antipaterns that help less to mow.
 
And just recently React brought us another gift. The next opportunity to mown less is getDeviredStateFromProps.
 
Technically - having a static mapping from the props to the state, the application logic should become simpler, more understandable, testable, and so on. In fact, many people began to stamp their feet, and to require prevProps back, unable (or without much desire) to redo the logic of its application.
 
In general, the abyss of hell unfolded. Previously, a simple task became more difficult.
 
getDerivedStateFromState - or how to make a complex  
github /reactjs.org , and was caused by the need to know exactly how the props were changed, for logging purposes
 
We have found a scenario where the removal of the componentWillReceiveProps will lead us to write worse code than we are currently doing.
 
 
//OLD WAY
componentWillReceiveProps (newProps) {
if (this.props.visible === true && newProps.visible === false) {
registerLog ('dialog is hidden');
}
}
//NEW WAY
static getDerivedStateFromProps (nextProps, prevState) {
if (this.state.visible === true && nextProps.visible === false) {
registerLog ('dialog is hidden');
}
return {
visible: nextProps.visible
};};
}

 
PS: But do you know that such operations should be performed in `componentDidUpdate`?
 
 
But that was only the beginning. On the same day was (3e3354). issue
was created. about the modification of getDerivedStateFromProps, because without prevProps there is no life. Exactly the same issue was already closed once with "Wont fix", and this time, after a long verbal battle, it was again closed with "Wont fix". It serves him right.
 
 
But, before discussing the way out, and why the issue was closed, it is better to think of a convenient example for clarity of reasoning.
 
 

Table. With sorting and page navigation.


 
 
Let's turn to TDD, and in the beginning we will define the problem, and the ways of its solution
 
 
 
What should I do to draw a table?
 
 
Take the data to display
 
Sort them
 
Take a slice, with data only for the current page
 
Do not confuse the order of items
 
 
 
What if the data has changed?
 
 
Start at the beginning of
 
 
 
And if only the page has changed?
 
 
Complete item 1.3 onwards.
 
 
 
How to change page
 
 
this.setState ({page})
 
 
 
How do I respond to a change in state.page?
 
 
No
 
 
 
 
 
That's the problem - you can react to changing props, but to change the state of such a function there (even if you read it in the title of this article).
 
 

The correct solution is number 1


 
More precisely, the "right" solution. I think it should be a finite state machine. Initially it is in state idle . When the signal is setState ({page}) it will transfer to another state - changing page . Upon entering this state, he will calculate what is there for him and will send the signal setState ({temporalResult}) . On good the machine should go to state «Next step» , which will count anything from the step after the current one, and eventually gets into commit , and where will transfer the data from temporalResult in data , then go to idle .
 
Technically, this is the right solution, and maybe it all works, somewhere deep in iron, or a piece of paper. Let it stay there.
 
 

The correct solution is number 2


 
And what if you create one more element, to which to transfer in the form of props state and props from the current element, and use getDerivedStateFromProps ?
 
That is, the "first" component is a "smart" controller in which the setState ({page}) occurs, and its dumb will not be so dump, computing the required data when changing external parameters.
 
All is well, but we do not implement the clause "to recalculate only what we need", since we KNOW that something has changed (because someone called getDerivedStateFromProps), but we do not know WHAT.
 
In this regard, nothing has changed.
 
 

The correct decision number 3 ("official")


 
The basis of the "decision", which served as arguments for closing the issue, was one simple statement.
 
You might not need redux getDerivedStateFromProps. You need memoization.
 
 
//base - https://github.com/reactjs/rfcs/pull/40#discussion_r180818891
import memoize from "lodash.memoize";
class Example {
getSortedData = memoize ((list, sortFn) => list.slice (). sort (sortFn))
getPagedData = memoize ((list, page) => list.slice (page * 1? (page + 1) * 10))
render () {
const sorted = this.getSortedData (this.props.data, this.props.sort);
const pages = this.getPagedData (sorted, this.props.page);
//Render with this.props, this.state, and derived values ​​
}
}

 
Memoization will follow the "changes", because it simply knows the "old" values, and calls the memo function Only when the value changes.
 
 
But there are two problems. And I took both from the second commentary to the original issue
 
 

Problem number 1 is


 
I'm having to resort to a weird multi-depth WeakMap, and making decisions about when to drop different levels of the cache.
The same "significant" order of change of values, multiplied by curved hands. There are some levels of caching, WeakMaps. Oho, what are you doing, stop!
 
 

Problem number 2


 
One solution is suggested by memoizing that computation and calling it each time, which is a good idea, in practice, it means managing caches which, when you are dealing with a function that takes more than one argument, greatly increases your surface area for potential bugs and mistakes.
 
 
And this is one of the main problems of all libraries of memoization - the requirements of using "finite" values ​​as arguments of the function. In general, just uncomfortable, but at the same time you can mix up the variable.
 
 
The first problem has a solution in reselect. In the cascades reselect , when having two memoized values ​​per input, it is possible to form the third memoized value on the output.
 
Even better is the composition of memoized functions, when you simply define the order of execution, and a certain (finite) automaton executes them one by one In general, the cascades reselect is also "composing", but they have a tree there, but a linear process is needed - waterfall.
 
Hmm, I saw a waterfall in the announcement of this article. What is it for?
 
 
const input = {this.state, this.props};
const resultOfStep1 = {input, sorted: this.getSortedData (input.data, input.sort);
const resultOfStep1 = {resultOfStep? sorted: this.getPagedData (resultOfStep1.sorted, resultOfStep1.page);

 
 
If "all garbage" is taken out to the hepler, then we get a fairly clean code
 
const Flow = (input, fns) => fns.reduce ((acc, fn) => ({acc, fn (acc)}), input);
const result = Flow ({this.state, this.props},[
({ data, sort }) => ({dаta: this.getSortedData(data, sort) });
({ data, page }) => ({dаta: this.getPagedData(data, page)
]);

 
 
A clean, simple and very beautiful solution for problem number ? clearly defining the order of formation of the final value, which is absolutely impossible to memoise.
 
Which it is completely impossible to memoise because the "step" of execution has only one argument, and with any change in input one must start from the very first stage - one can not understand that only the page has changed and only the last step needs to be restarted.
 
 

Or is it possible?


 
 
import {MemoizedFlow} from "react-memoize";
class Example {
getSortedData = (list, sortFn) => list.slice (). sort (sortFn)
getPagedData = (list, page) => list.slice (page * 1? (page + 1) * 10))
render () {
return (
?
? input = {this.props}
, flow =[
({data, sort}) => ({ dаta: this.getSortedData(data, sort)}),
({data, page}) => ({ dаta: this.getPagedData(sorted, page)});
].
> {({data}) => this is the data you are looking for {data} }
.
.
.)
}
}

 
 
It's not strange - this time everything will work like clockwork. And even the Flow function that will be used to calculate the final value will be exactly the same as before.
 
The whole secret is in another function of memoization, memoize-state, about which I told a month ago - she then knows what parts of the state were used at a particular stage, giving the opportunity to re-use memeble waterfall.
 
More complex example to play - codesandbox.io/s/23ykx5z5jp
 
 
As a result, the static function getDerivedStateFromProps is replaced with (in a certain sense) a statically defined component, the configuration of which allows you to clearly define "Method and method" obtaining the result, or more precisely forming the final result from the set of initial data.
 
It can be getDerivedStateFromProps, getDerivedStateFromState, getDerivedPropsFromProps - anything. You can even run the side effects (it works, but it's better not).
 
And most importantly - this approach allows you to determine exactly the reaction to the change in the parameter. And it allows you to determine exactly in the form that the "correct"
 
Data should be updated if the data, or page, has changed. And not only if the "page".
 
One definable Flow can not be broken. The main thing is to stop wanting to know the old meanings.
 
 

Conclusion


 
In general, React recently teaches us "not to want" various approaches that can lead to a gob-code, or problems with an asynchronous rendering. But people remain people, and do not want to give up old, time-tested approaches. This is the problem.
 
 
In fact, sometimes it is very difficult to understand how to "correctly" prepare a reagent today, because literally two weeks ago you prepared it, and here the BAC and the recipe changedI.
 
But do not despair - memoize-state and react-memoize built on its basis a little dull pain. All problems can be solved, the main thing is just to try to look at the problem from a different angle.
 
 
PS: That very original issue with the conclusion is github.com/reactjs/rfcs/pull/40#discussion_r180818891
 
PS: A little bit about how and why memoize-state works - habrahabr.ru/post/350562
+ 0 -

Add comment