This is a translation of the article “ What's So Great About Redux? ” ( By Justin Falcone ), which seemed to me very pleasant and interesting to read, enjoy!
Redux masterfully manages complex state interactions that are difficult to convey using the state of the React component. In fact, this is a messaging system, which is also found in object-oriented programming, but it is not embedded directly in the language, but implemented as a library. Like OOP, Redux transfers control from the caller to the receiver — the interface does not directly control the state, but sends a message to it for processing.
In this regard, the storage in Redux is an object, the reducers are method handlers, and the actions are messages. Calling store.dispatch({ type:"foo", payload:"bar" })
equivalent to store.send(:foo, "bar")
in Ruby. Middleware is used in much the same way as in aspect-oriented programming (for example, before_action
in Rails), and dependency injection is implemented using connect
in react-redux.
What are the benefits of this approach?
Thanks to the inversion of controls mentioned above, there is no need to update the user interface as soon as the implementation of the state transition changes. Adding such complex functions as logging, canceling an action or even time-travel debugging becomes easy. Integration tests are only to ensure that the correct action is sent, and for the rest of the unit tests are sufficient.
The state of the components in React is too slow to work with end-to-end functionality that affects many application modules, such as user information or alerts. For this, Redux has a state tree independent of the user interface. In addition, when processing a state outside the interface, it is easier to maintain consistency, because serialization to localStorage or URL is performed in a single place.
method_missing
.But all these cases are special. What about simpler scripts?
This is where we have problems.
Actions can be viewed as complex transitions between states, but basically they only give one value. In applications made on Redux, many such simple actions often accumulate, and this clearly resembles Java with writing the functions of the setters manually.
The same state fragment could be used throughout the application, but in most cases it refers to one specific part of the interface. Transferring states from components to the Redux repository is just an additional redirection without the proper level of abstraction.
However, the main catch is that after such a long work on boilerplates for simple cases, you generally forget that there are better solutions for difficult cases. Faced with an ingenious state transition, you end up sending a dozen different actions that simply set new values. You copy and paste switch options from one reducer to another, rather than abstracting them as functions that can be used from everywhere.
It is easy to blame it all on the human factor: I didn't master the documentation, or “the master is stupid - the knife is stupid” - but such problems are often suspicious. Is there a problem in the knife if he is stupid for most masters?
It turns out, it is better to avoid Redux in normal cases and save it for special ones?
This is the advice that the Redux team will give you - and I say the same to my colleagues: do not hold on to it until setState
starts to get out of control altogether. But even I myself do not follow my own rules, because you can always find a reason to use Redux. Maybe you have many actions like set_$foo
, while with each value assignment the URL is updated or some intermediate value is reset. Maybe you have a clear one-to-one correspondence between the state and the user interface, but you need to log or cancel actions.
In fact, I do not know how to write on Redux, and especially how to teach it. Every application I worked on was full of such antipatterns from Redux — either I myself could not find a better solution, or I couldn’t manage to persuade my colleagues to change something. If, one can say, the Redux expert writes mediocre code, then what about the newbie. I'm just trying to balance the popular approach of using Redux for everything, and I hope that everyone will develop their own understanding of Redux.
What to do in this case?
Fortunately, Redux is flexible enough to connect third-party libraries to it with simple things — like Jumpstart ( https://github.com/jumpsuit/jumpstate ). Let me explain: I do not think that Redux should not be used for low-level tasks. It's just that if work on them is given to third-party libraries, this complicates understanding, and according to the law of triviality, time is spent on trifles, because each user eventually has to build their own framework in parts.
Some like it
And I am among them! But this does not apply to everyone. I'm a big fan of Redux and use it in almost all projects, but I also like to try new webpack configurations. I am not a typical user example. Creating your own abstractions on the basis of Redux gives me new opportunities, but what can abstractions give, written by some Senior-engineer who left no documentation and left six months ago?
It is quite possible that you will not encounter difficult problems that Redux solves so well, especially if you are a newcomer (junior) in a team where senior colleagues are engaged in such tasks. Then you imagine Redux as such a strange library, with which everything is rewritten three times. Redux is simple enough to work with it mechanically, without a deep understanding, but the joys and benefits of it are few.
So I come back to the question I asked earlier: if the majority of people use the tool incorrectly, isn't the whole problem in it? A quality tool is not only useful and durable - it is also a pleasure to work with it. Use it correctly most conveniently. Such a tool is made not only for work, but also for a person. The quality of the tool is a reflection of the care of its creator about the master who will use it.
And how do we take care of the masters? Why do we claim that they are doing everything wrong, instead of working on the convenience of the tool?
In functional programming, there is a similar phenomenon that I call the “curse of the lesson about monads” : it is very simple to explain how monads work, it’s simpler than simple, but to tell you what their benefits are.
Are you seriously going to explain monads in the middle of a fast?
Monads are a common abstraction in Haskell that is used for a variety of computations, for example, when working with lists or handling errors, states, time, or input / output. Syntactic sugar in the form of do-notation allows representing sequences of operations on monads in a form similar to imperative code, much like generators in JavaScript, which make asynchronous code similar to synchronous.
The first problem is that it is not entirely correct to describe monads in terms of their use. Monads appeared in Haskell for working with side effects and sequential evaluation, but in the abstract sense they have nothing in common with this: they are simply a set of rules for the interaction of two functions, and there is no special meaning in them. Similarly, associativity applies to arithmetic, operations on sets, combining lists and null propagation, but exists independently of them.
The second problem is verbosity, which means, at a minimum, the visual complexity of monads compared to the imperative approach. Well-defined optional types like Maybe
are safer than looking for pitfalls as null
, but the code with them is longer and more awkward. Error handling using the Either
type looks clearer than the code in which an exception can be thrown anywhere, but, we agree, the code with exceptions is much more concise than the constant return of values from Either
. As for the side effects in the state, input / output, etc., they are completely trivial in imperative languages. Fans of functional programming (I am among them) would argue that working with side effects in functional languages is very easy, but you can hardly convince anyone that any kind of programming is easy.
The benefits are truly noticeable if you look at these examples with a broad eye: they do not simply follow the laws of monads — they are the same laws. A set of operations that works in one case can work in the rest. Turning a pair of lists into a list of pairs is conventionally the same as merging a pair of Promise
objects into one that returns a tuple with the results.
So why all this?
The fact is that with Redux the same problem is that it is difficult for him to teach, not because it is complicated, but just because it is simple. Understanding is more likely not in knowledge, but in trust in the basic principle, thanks to which one can arrive at everything else by induction.
This understanding is not easy to share, because the basic principles are reduced to banal axioms (“avoid side effects”) or are so abstract that they almost lose their meaning ( (prevState, action) => nextState
). Specific examples do not help: they only demonstrate the verbosity of Redux, but do not show its expressiveness.
Having achieved enlightenment, many of us immediately forget how they came to him. We no longer remember that our “enlightenment” is the result of repeated mistakes and delusions.
And what do you suggest?
I want us to realize that we have a problem. Redux is simple, but not easy. This is a justified decision of the developers, but at the same time a compromise. A tool that would partially sacrifice the simplicity of a mechanism in favor of ease of use would be useful to many people. But the community often does not even recognize that some sort of compromise was made at all.
In my opinion, it is interesting to compare React and Redux: although React is much more complex than Redux and its API is much broader, oddly enough, it is easier to learn and use. All that is really needed in the React API is the React.createElement
and ReactDOM.render
, and the state, life cycle of components, and even DOM events can be handled differently. The inclusion of all these features in React made it harder, but also better.
The “atomic state” is an abstract idea, and it only brings practical benefits after you understand it. On the other hand, in React, the component method setState
controls the atomic state for you, even if you do not understand how it works. Such a solution is not ideal - it would be more efficient to abandon the state in general, or it is mandatory to update it with each change. Also, this method may present unpleasant surprises if called asynchronously, but still setState is much more useful for React as a working method, and not just a theoretical concept.
Both the Redux development team and its user community actively oppose the expansion of the API, but sticking together dozens of small libraries, as is being done now, is a tedious task even for experts and a dark forest for beginners. If Redux cannot grow to the level of built-in support for simple cases, it will need a “savior” framework that will occupy this niche. Jumpsuit could be a good candidate - it embodies the ideas of actions and states in the form of specific functions, while maintaining the nature of the many-to-many relationship. But the choice of savior is not as important as the fact of salvation itself.
The irony is that the sense of existence of Redux is the “Developer Experience”: Dan developed Redux to study and reproduce the time-travel debugger as in Elm. However, as the idea acquired its own form and turned, in fact, into the object-oriented environment of the React ecosystem, the convenience of “DX” faded into the background and gave way to configuration flexibility. This caused the flowering of the Redux ecosystem, but where there should be a convenient framework with active support, there is a gap in the void. Are we, the Redux community, ready to fill it out?
Translated by Olga Isakova
Source: https://habr.com/ru/post/333848/
All Articles