I love React. I love how it works. For doing things "right." HOC, Composition, RenderProps, Stateless, Stateful - a million patterns and anti-patters that help to mow less.
And just recently React brought us another gift. Another opportunity to mow less - getDeviredStateFromProps.
Technically, having a static mapping from props to the state, the application logic should become simpler, more understandable, testable, and so on. In fact, many people began to stomp their feet, and demand prevProps back, unable (or without special desire) to remake the logic of their application.
')
In general, the depths of hell unfolded. Previously, a simple task has become more difficult.
The initial discussion turned on the pages of
github / reactjs.org , and was caused by the need to know exactly how the props changed, in order to log
If you’re not sure, you’ll have to do so.
PS: But do you know that such operations should be performed in `componentDidUpdate`?
But that was only the beginning. On the same day, a (re)
created issue of the getDerivedStateFromProps modification was created, because there is no life without prevProps. Exactly the same issue was already closed once with “Wont fix”, and this time, after long verbal battles, 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 come up with some convenient example for clarity of reasoning.
Table. With sorting and page navigation
Let us turn to TDD, and at the beginning define the problem, and ways to solve it.
- What you need to do to draw a table?
- Take data to display
- Sort them out
- Take slice, with data for current page only
- Do not confuse the order of items
- What if the data has changed?
- Start over again
- And if only the page has changed?
- Run point 1.3 and on.
- How to change page
- this.setState ({page})
- How to respond to a change in state.page?
- No
That's the problem - you can respond to a change in props, but to change the state there is no such function (even if you read it in the title of this article).
The correct solution is number 1
More precisely, the "correct" solution. I think it should be a state machine. Initially, it is in
idle state. When a
setState ({page}) signal
arrives, it will transition to another state - the
changing page . When entering this state, he will consider what he needs there and send a signal
setState ({temporalResult}) . For good, then the machine should go to the
“next step” state, which will calculate anything from the step after the current one, and eventually get into a
commit , and where it will transfer data from
temporalResult to
data , and then go to
idle .
Technically, this is the right decision, and everything is possible and works, somewhere deep in the gland, or a piece of paper. Let it remain there.
The correct solution number 2
And what if you create another element, in which you transfer in the form of state and props from the current element, and use
getDerivedStateFromProps ?
Ie the “first” component is the “smart” controller, in which setState ({page}) occurs, and its dumb will not be such a dump, calculating the necessary data when the external parameters change.
Everything is good, but the item “recalculate only what is needed” is not realizable, since we KNOW that something has changed (because someone called getDerivedStateFromProps), but we don’t know WHAT.
In this regard, nothing has changed.
The correct decision number 3 ("official")
The basis of the “solution”, which served as the argument for closing the issue, was one simple statement.
You might not need redux getDerivedStateFromProps. You need memoization.
Memoization will keep track of the “changes” because it simply knows the “old” values, and calls the
memos function only when the value changes.
But there are two problems. And I both took from the second comment to the original issue.
Problem number 1
WeakMap,
The same "significant" order of change of values, multiplied by the curves of the hand. Some caching levels appear, WeakMaps. Oho, what are you doing, stop!
Problem number 2
It means that you can’t mistakes.
And this is one of the main problems of all libraries of memoization - the requirement of using "finite" values as function arguments. In general, it is simply inconvenient, but at the same time it is possible to confuse variable.
The first problem has a solution in reselect.
In reselect cascades , when you have two memoized values at the input, you can create a third memoized value at the output.
Even better is the composition of memoized functions, when you simply determine the order of execution, and a certain (finite) machine executes them one by one ... In general, reselect the cascades is also “composing”, but they have a tree there, and here you need a linear process - 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 = {... resultOfStep1, sorted:this.getPagedData(resultOfStep1.sorted, resultOfStep1.page);
If “all the garbage” is brought to the hepler, then we get fairly clean code
const Flow = (input, fns) => fns.reduce( (acc,fn) => ({...acc, ...fn(acc)}), input); const result = Flow({...this.state, ...this.props },[ ({ data, sort }) => ({data: this.getSortedData(data, sort) }); ({ data, page }) => ({data: this.getPagedData(data, page) ]);
A clean, simple and very beautiful solution for problem number 1, clearly defining the order of formation of the final value, which is completely impossible to memorize.
Which is completely impossible to memoize because the “step” of execution has only one argument, and with any change in input it is necessary to start from the very first stage - it is impossible to understand that only the page has changed and only the last step needs to be restarted.
Or can it?
import {MemoizedFlow} from "react-memoize"; class Example { getSortedData = (list, sortFn) => list.slice().sort(sortFn) getPagedData = (list, page) => list.slice(page*10, (page+1)*10)) render() { return ( <MemoizedFlow input={this.props} flow = [ ({data, sort}) => ({ data: this.getSortedData(data, sort)}), ({data, page}) => ({ data: this.getPagedData(sorted, page)}); ] >{ ({data}) => <table>this is data you are looking for {data}</table> } </MemoizedFlow> ) } }
It is not strange - this time everything will work as a watch. 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 knows what parts of the state were used at a particular stage, making it possible to realize the memoded waterfall.
More complicated example of how 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 setting of which allows you to clearly define the
“method and method” of obtaining the result, or rather the formation of the final result from the source data set.
It can be getDerivedStateFromProps, getDerivedStateFromState, getDerivedPropsFromProps - anything. You can even run side effects (works, but it's better not to).
And most importantly - this approach allows you to determine exactly the response to a parameter change. And allows you to determine exactly in the form that "correct"
The data must be updated if the data has changed, or the page. And not only if the "page".
Once defined, Flow cannot be broken. The main thing is to stop wanting to know the old values.
Conclusion
In general, React recently teaches us to "not want" various approaches that can lead to shitty, or problems with asynchronous rendering. But people remain people, and do not want to abandon the old, time-tested approaches. That's the problem.
In fact, sometimes it is very difficult to understand how today it is “right” to prepare the reactor, because literally two weeks ago you were preparing it, and here the BAM and the recipe changed.
But do not despair - a
memoize-state and a
react-memoize built on its basis will dull the pain a bit. All problems can be solved, the main thing is just to try to look at the problem from a different angle.
PS:
The very original issue with the conclusion .
PS:
A little about how and why memoize-state works .