Redux’s state management philosophy is elegant, and the time travel debugging support that comes with it is cool. But is this feature the legendary silver bullet, and what additional burden does it place on users? Let’s rethink that.

What is time travel?

At the React Europe conference in 2015, Dan Abramov showed a Demo of Redux DevTools that allows developers to freely navigate historical states to improve the debugging experience, and the experience was amazing and well received. Since then, Vuex and MobX state management libraries have introduced support for similar functionality in their debugging tools.

We can think that in the field of front-end state management, the narrow concept of “time travel” refers to the function of tracing back in the history state at will during development after meeting the following premises:

  • Unify local state to global store for state management.
  • The development environment has DevTools installed with the state management library or special monitoring components introduced.
  • HMR hot loading of Webpack is enabled in the development environment.

It is important to note that this feature is entirely for debugging purposes. However, this capability is so impressive that it’s one of the main reasons many people switch to the React + Redux stack: with a nice conceptual model and a nice debugging experience, it’s a workaround. Just as React was the first to implement declarative rendering in the browser, Redux was the first to implement the ideal debugging experience in the browser. These original efforts have contributed significantly to the front end. The following analysis of some potential problems with React + Redux is also based on respect for community work.

Why don’t you need time travel?

In the just-concluded D2, I didn’t see a completely new wheel, but I got new answers to a lot of open questions. One of these questions helped me reframe my understanding of the front end and forms the main argument for this section. The question is: How do you classify complex front-end applications?

Traditionally, we’ve looked at functionality as a dimension that distinguishes app categories. For example: management background, activity H5, CHAT IM, e-commerce shopping, video live…… We have so many segments, each with different business pain points and priorities, that it is difficult to “get through the two channels”. But is there an easier way to divide? Here, we have a simpler answer, namely complex front-end applications are simply divided into two categories: data-driven and event-driven.

Data-driven front-end applications

The business complexity of such applications comes entirely from the endless data and complex business processes in the background. For example, the browsing page of a shopping website does not have much input to process, but the product data from the back-end interface can be thousands of pages; Another example is the booking platform of 12306. Although its front-end interface is simple, the complexity of the whole business process may be beyond the imagination of ordinary users and even developers. In general, applications like these, which require users to fill out a few forms and a captcha at most, often have a deep hole in the business logic that only those who have dabbled in it can understand. These applications can be understood as data-driven.

Event-driven front-end applications

In contrast, event-driven front-end applications derive their complexity from user input events. For example, a rich text editor can become a legendary “sinkhole” even if it does not connect to the background interface at all when editing, just processing the user’s pasting, selecting and keyboard events. For example, an H5 version of “Tai Drum Master” game only needs to pull static music resources from the back end, but as long as the rhythm of the user clicks is tens of milliseconds different, the interface state and the final result may be completely different. When building this kind of application, the difficulty mainly comes from the large number of different types of asynchronous events can be arbitrarily arranged and combined, so that the possible state space is extremely large and prone to error – I believe that anyone who has maintained several timers in the page can understand. We can classify such applications as event-driven.

Time travel and application classification

What does the concept of time travel have to do with the two categories of applications mentioned above? This relates to many of the motivations for using Redux in the technology selection: The Redux development tool supports time travel, so there is less risk of our application going to Redux in a scenario that requires state backtracking.

This does sound like a lot of late expansibility, but what’s wrong with it? Once we rethink the categorizing dimensions of our applications, the need for time travel capabilities is radically different:

  • Data-driven front-end applications require almost no time travel capability. Since the data from the back end is essentially the Single Source of Truth, the backtracking operation based on the state management tool in the front end is very easy to destroy the dependence on the data Source, resulting in inconsistent state of the front and back ends. A very simple example: if the form page of a admin backend application supports time travel, then the “travel” replay of the form submission event will obviously result in repeated POST requests, and this is not an idemidemous operation, so front-end time travel may even violate RESTful concepts.
  • Event-driven front-end applications that rely heavily on time-travel-like technologies. Almost every rich text editor on the market maintains its own undo stack — the core functionality of time travel! For another example, the game’s ability to save and read progress is also a typical time travel feature. For this type of application, time travel is even one of the core elements of the experience: how can you trust a rich text editor with inexplicable formatting problems when retracted? Not to mention a game that can’t read past progress. Even, if undo is implemented well, users can undo themselves when they encounter unexpected behavior or even editor bugs, and then try other interactions to achieve their goals — time travel is the ultimate guardian of the user experience!

As we can see from the above discussion, only for event-driven front-end applications does time travel make sense (and a lot of sense!). . For data-driven front-end applications such as administrative backends, time travel is just the icing on the cherry on top — in this business scenario, it’s a stretch to cite time travel as a big reason to choose Redux.

I believe many of you will argue that there are many successful cases of using Redux to manage back-end business. Do you think their architects are all wrong? Also, Redux has many additional benefits besides time travel, which are far more important than time travel when making decisions. Sure, Redux’s popularity has proven it can support “large-scale” front-end applications, but the framework must have been designed with trade-off in mind. In a business scenario where time travel is not required, some of the framework designs introduced in Redux to enable time travel pose additional problems. So here’s the question: What did Redux sacrifice to be the first to implement time travel?

What is the burden of the time travel tech stack?

She was too young then to know that all the gifts of fate had been secretly priced.

— The Beheaded Queen

When Redux solved the problem of moving layers of React props, we were excited: wow, this stateless component is so elegant! Wow! Once all the states are in the Store, we can go back and forth as smoothly as we want! Soon, two best practices emerged:

  • Wherever possible, write stateless components whose state is managed by the global Store.
  • The data structure of the global Store should be as flat as possible.

So what’s the problem with apps that follow these two best practices?

Antipatterns for global state

It would be tempting, given the lure of time travel, to hand over all states to the Store and kill setState entirely: not only would it support time travel perfectly, but it would also solve a seemingly annoying problem with React. There is no “right” answer for this in Redux’s official documentation. That is to say, it is reasonable to refer all states to the store. But is it really true?

I don’t know how many students in the beginning of programming, heard the predecessors warned: use less global variables. The React stack, however, looks like a fancy global state, but it’s just a Context with a new global variable. Do you think people won’t recognize you if you’re wearing a Store? None of the problems with global variables can be avoided:

  • Global state is easy to cause naming conflicts, which is evident in a flat store: The various Redux encapsulation frameworks also like to define their own naming conventions to ensure “consistency”, but if naming is not guaranteed by the language’s scoping mechanism itself, but by fragile conventions, it is an artificial burden: There’s nothing wrong with Hungarian nomenclature in assembly language without scope, but isn’t it really a step backward in history to maintain this level of convention in software engineering in 2017? — Of course not! Can assembly language support time travel?
  • Global state is difficult to expressNested data types. Update in the Redux family bucket{a: {b: {c: {d: 1 }}}}It is almost necessary to use assistive tools. For a rich text editor to express nested tables in a table, the corresponding native JSON data structures are pretty thin and must be Immutable — but why don’t I just use Immutable and skip the Redux layer? This is what slate.js does. Oh, you mean Facebook’s own draft.js? It is Immutable, but it implements elegant flat data structures that do not support tables.
  • The memory model of global state does not conform to the classical computer architecture. For a desktop GUI that is much more complex than a web page in a browser, is the memory space of each window corresponding to a process independent of each other, or intermixed in a time-travel-enabled “global state”? This is a sign of the backwardness of desktop operating systems. Can Mac and Windows time travel as gracefully as the pages we write based on Redux?

By now, there are plenty of reasons to be skeptical of Redux’s idea of a flat global Store. While there is no direct correlation between store design and time travel, the temptation to “elegant” global states that are “easy to debug, easy to reason about, and easy to understand” can lead developers into bigger traps. That’s something to worry about.

Of course, Redux does address one pain point, the problem of state communication between deeply nested components. But solving this problem doesn’t mean we have to take the state to the global level. This problem can be simply interpreted as: the method implemented in component A, the event that triggers it is in component B, and component C needs to subscribe to the execution result… React is tricky, but placing the store in the top of the A, B, and C components — not globally — and customizing the Context is enough to solve the problem.

Time travel with Boilerplate

On the other hand, a common criticism of Redux is that it has a lot of Boilerplate code, which requires Action, Reducer and Middleware to send a simple request, which is a heavy mental burden. This detail has a subtle relationship to how time travel is implemented. In short, it can be interpreted as Redux sacrificing the development experience for the debug experience:

In Dan Abramov’s talk, he mentioned an important capability brought by the combination of Webpack HMR and Redux DevTools: Once you make changes to a Reducer code, all actions will be re-evaluated and updated.

We can understand the granularity of HMR as the hot replacement at the function level (it is not well understood by the author here, please be sure to point out any errors), and the minimum granularity of Redux to realize the state management logic is just a pure function like Reducer. Therefore, for Dan himself, to implement the feature of “as soon as a function is found to be patched, run all the Action in JSON format again” on Redux architecture, There was no need for clever manipulation — so he implemented Redux DevTools in a week, and it was really good! The trade-off is that developers using Redux must use this onerous mechanism during development to make it easy for Dan to improve the debugging experience… There is no absolute right or wrong choice in technology, and there is no comment here on the trade-off between development and debugging costs.

Time travel doesn’t work out of the box

In addition to some problems with the way Redux supports time travel, another pitfall is the thought, “Redux DevTools supports time travel well, so it shouldn’t be too hard to integrate into my app.” As mentioned earlier, time travel is particularly important when implementing an event-driven front-end application. But the difficulty of implementing this feature is probably not as simple as pulling in a Redux. Taking rich text editing as an event-driven application as an example, here are a few specific business examples:

  • When using Slate.js, the undo stack can be accidentally emptied in some cases. After reading the source code, we found that the undo stack implementation at the time would push the editor initialization changes as the first item on the stack. When you try to undo this item, the side effect is that you accidentally break the editor’s counting logic and lose something that could have been redone. We have solved this bug by mentioning PR, but there are still many similar issues of cancellation stack details.
  • Some business scenarios are difficult to undo and redo with basic stack operations like push and POP. For example, in the process of uploading pictures, the user can still input text. In this case, the undo event operation of “progress bar progress change” will “mix” with the user’s input event in the undo stack, making it more difficult to undo.
  • Different deprocessing is required for successive input events. For example, when the user enters a line of text consecutively, the whole line needs to be undone at once. If the user is slowly typing word for word, then you should undo word for word.

In each of these scenarios, the solution to each case has little to do with Redux’s philosophy. For more complex scenarios (such as real-time collaboration for rich text editing), the basis for implementing time travel is no longer simple undo stack + full state replacement, but involves advanced algorithms such as OT, which are enough to write many papers. In this case, there are two obstacles to implementing time travel type functionality in event-driven applications:

  • Redux’s native mechanism does not have a very specific solution to even the base case of this requirement.
  • For the advanced case of this requirement, the solution is almost completely Redux independent.

So the summary of the problem here is ironic: Redux doesn’t do much for applications that require time travel beyond introducing its set of conventions. Combined with the above discussion, you can see that time travel is almost completely unimplemented in data-driven applications, and Redux is of limited help in event-driven applications…

What alternatives do we have?

This article is not intended to sell a new wheel, but we did find that there are more appropriate state management options for both of the above application scenarios. MobX and RxJS are the two libraries that I had a preference for, and upon re-examining the scene, they both have their strengths:

MobX and data-driven applications

In data-driven applications, domain models are likely to be very fragmented and numerous (for example, each form can have its own data model), and for each domain model, the ability to encapsulate the corresponding add, change, and delete capabilities is almost sufficient to meet the requirements. At this point, MobX’s abstraction of state management seems very natural:

  • Class – based data model structure, can be very easy to encapsulate each model of add, change, delete operations. It is also very easy to instantiate multiple instances of different stores and inject them into desired components. For interstore communication, inject a reference to RootStore when instantiating a substore.
  • Ts-based type declarations are far more advanced than the primitive string constants + native JS objects in Redux.
  • The dependency tracing based update mechanism can precisely update components as needed when an object property is updated. In a normal business scenario, this is better performance than Diff operations that change the state in full. For reference, it took a lot of redrawing and Dan Abramov himself to optimize the Redux implementation to get MobX right out of the box.

Note that MobX’s performance advantage when redrawing comes at the expense of greater memory footprint after access hijacking. For this tradeoff, I also happened to ask the UC kernel developer lecturer sharing Web optimization on D2 about the impact of memory footprint on front-end performance. According to Dalao, the main cases in this area are still obvious anti-patterns such as mass downloading of images, while memory consumption of the data model in state management is not a performance bottleneck. From this perspective, MobX’s design trade-offs can be considered worthwhile.

RxJS and event-driven applications

In event-driven front-end applications, it is very important to grasp asynchronous logic. Libraries like Redux-Saga offer some ways of dealing with asynchronous side effects in this regard, but if you get to know RxJS, Saga’s seemingly powerful capabilities are a toy when compared to Rx’s event-flow mindset.

If you think of RxJS in terms of data-driven applications, you’ll just feel that its API is heavy and intrusive. In fact, you need to feel the power of this set of ideas in event-driven scenarios. Here is an example of how elevators are scheduled daily while waiting for an elevator: the state of the elevator is directly determined by the flow of events when the user presses the floor button, and RxJS responsive programming makes sense to model this service. As a tutorial to learn RxJS from examples, I have written a column called “Introduction to Responsive Programming: Implementing the Elevator Scheduling Simulator”, and a supporting Demo implementation, welcome interested students to read.

conclusion

There is no doubt that time travel is a powerful debugging feature. This article discusses some of the issues involved in moving time travel from debugging tools to business: data-driven front-end applications don’t need it much; The fact that Redux implements time travel gives rise to some anti-patterns; The other technical details involved in implementing time travel are well beyond Redux’s scope. As alternatives, MobX, a state management tool based on OO, and RxJS, based on responsive programming, are my favorites in different scenarios. For GraphQL and other new wheels that are not mentioned in this article, I hope dalao, a reader with relevant experience, can kindly advise.

This article seems to be taking aim at Redux everywhere, and while there are certainly some interests here (I’ve never liked it much and use it less than MobX, RxJS or even Vuex), the conclusions are backed up by real-world scenarios, It’s definitely not as hard as the idea that the Redux API is so hard to learn that it must suck. And the work of the Redux team is very honorable. If there is any deviation in the understanding of Redux and time travel in the article, please point out that I am also willing to revise and optimize my ideas according to the discussion.

The last thing is my understanding of the front-end “circle” : I find that many people in this field have a blind worship of the frameworks and tools they use daily: they don’t allow others to comment on the problems of their frameworks; Explain the design problem of the frame as a metaphysical problem of “you are not good at it because you are not good enough”; Labeling similar tools as “bad”… Perhaps this does reflect a certain “dedication and love” for the front end, but it also makes the discussion atmosphere in the Chinese community look bad compared to that abroad. One of my favorite open-ended interview questions is “What’s wrong with your preferred framework?” “(Many mediocre candidates often respond with” I don’t see anything bad “in an attempt to demonstrate their familiarity with the framework…) And reverse thinking actually helps us to understand the principles and trade-offs of frame design in combination with the actual scene.

Thank you for keeping up with us. I hope you found this article helpful