lizheming

The Ugly Side Of Redux – codeburst

lizheming · 2017-12-23推荐 · 872阅读 CET/4 CET/6 原文链接

The Ugly Side Of Redux

In this post I will briefly explain Redux at a very high level for those of you who don’t already know it, I will try to convince you why Redux is actually promoting an anti-pattern (the singleton global store), and I’ll show you my own alternative called Controllerim.

What is Redux

Let’s start from the very beginning and try to define Redux. In the Redux repo you can find this definition:

Redux is a predictable state container for JavaScript apps.

Simple isn’t it? You just take Redux and put a state inside it, and everything becomes predictable! Just kidding. If you are like me, this definition didn’t help you at all with understanding what is Redux. The Redux repo now suggests you to watch a series of 30(!) videos of “getting started” (total time of two hours), and believe me, after watching those 30 videos you’ll still be confused, So it’s better if you continue to the other 27 videos that help you understand how to build a simple React app using Redux.

A Screenshot from the Redux repo on github

But before you dive into the videos, let me try defining Redux for you, this time with my own words.

What is Redux, take 2

Every application has a state. There are many ways to manage the state of your app, and Flux is one such a way- it is a design pattern for managing the state of your app while keeping your data flow in a unidirectional way. Redux takes Flux and gives you an opinionated way to implement it. It doesn’t give you lots of tools, that’s why it’s so small. It’s basically just telling you “do this, do that”, while most of the work is being done manually by you, the developer. So Redux is something between a design pattern by itself (by telling you what to do) and a minimal library with some useful tools that help you follow the design pattern (create store, connect etc’.).

Why do we need to manage our state?

Many people jump into Redux without really considering why they need it. Why do we need to manage our state in the first place? The answer is that it’s all about sharing a state between different components while keeping everything reactive. That means, any change to the shared state should be reflected by all the views that make use of that state.

In a perfect world, every component would be fully encapsulated and would hold its own state, without exposing it to anybody. In that case, React’s local component’s state would suffice, and we wouldn’t need an external state management tool. React reassures for us that every change to the component’s state will immediately be reflected by the view, and everything just works.

The problems begins when we need to share a state between components. One way to do this is using props- if component A wants to share something with one of its children, component B, it can pass it to B through props. But this way doesn’t scale well in a big, complex app where you have lots of data to share and you need to pass props down to deep nested children (and be very careful not to accidentally override props with same name).

The Redux way

Redux tackles this problem by using a global store to hold the state. All the components can connect to the store and make use of the data inside it, while redux makes sure to update all the connected components on every change of the state, thus keeping all the views reactive.

So what are the problems with this global store solution?

  • No encapsulation: If you want to share some prop, you have to put it in the global store and it immediately becomes visible to all the other components (in Redux terms- every component can connect to that data), even components that don’t need to know about this prop.

  • The store’s life cycle is different from the components’ life cycle: Let’s say we have a ShoppingCart component, and that we need to share some data about it, for example isCartFull. So we put isCartFull in the global store. Now let’s say that we fill up our shopping cart with products and continue to the purchase page. Eventually we pay, and the shopping cart component is no longer needed. What happens now is that the global store still holds a variable isCartFull that is now set to true, and we must remember to clear this flag once a new ShoppingCart component enters the screen. We must manually keep cleaning the store from garbage of old components that already left the screen, and trust me- countless of bugs out there are caused by someone forgetting to clean the store.

  • The global store is a singleton, but a component may have many instances: Let’s continue with the shopping cart example. What do you think will happen, if for some awkward reason, you need to display two shopping carts on the screen simultaneously? There is only one isCartFull flag in the store, so obviously we need to make some changes. We can create two flags, isCartAFull and isCartBFull, or we can make something more generic and create an array of flags, and each cart will receive via props an index to look for in the array. But what does all of that mean? we just wanted to create another instance of shoppingCart and suddenly things turned ugly. Why? Because the shopping cart is not fully encapsulated, and thus not fully reusable! The global store is a singleton, while the shopping cart can have multiple instances. That’s one reason why singletons are most of the time considered an anti-pattern and should be avoided when possible.

Those are only the problems with the global store architecture of Redux, but there are many more problems in the implementation itself:

  • Learning curve: As I mentioned before, many developers found themselves lost in the Redux world of strange terms, weird entities and the connection between them: thunk, selectors, reducers, actions, middlewares, mapStateToProps, mapDispatchToProps, etc’. Learning all this stuff is not easy, and combining all of this together correctly takes time and practice.

  • Flow complexity: Many developers find it hard to follow the flow of a Redux app. There are just too many files and everything is just too fragmented that it has become quite hard to understand what is going on.

  • Boilerplate: There is a H-U-G-E amount of boilerplate code in every Redux application.

The Controllerim alternative

As I promised, I am now going to show you what I personally think is a better approach to manage your state .

Controllerim is my own state management tool that promotes the use of controllers instead of stores.


The good old MVC pattern

In Controllerim, we don’t use singleton store anymore. Instead, every component has a controller that holds its state, in a way that promotes separation of business logic from the view, and allows sharing the state between components in a safe and easy way, while keeping everything reactive and testable.

Let’s start with a visual explanation of the different approaches:

Redux’s global store approach:

Redux global store

The red arrows represent the hierarchical relations between the components, and the black arrows represent the connections of the components to the global store. You can see how every component is connected directly to the global store, and the global store life cycle is independent of the components’ life cycle.

Controllerim’s approach:

Controllerim’s local controllers approach

In this diagram, you can see that every component holds a reference to its own controller in a way that the controller’s life cycle is bound to the component’s life cycle (when the component dies the controller dies with it). The green arrows represent both relations between components and relations between controllers, and all the relations are transitive- if component A is a parent of component B, and component B is a parent of component C, that means that component C can make use of component’s A controller. The last sentence was a bit confusing, But all it means is that every component can make use of data (get and set) from all its parents, not necessarily its direct parent. In our example, the components in the 3rd level (root is level 0) can interact with their direct parent’s controller and with the root’s controller, but not with their siblings’ controllers.

Note: this approach is a superset of the global store approach: every component can fetch data from the root controller, so if you put all the data in the root controller, we are falling back to the Redux approach of global state. Keep in mind that the root component of your app lives for as long as the app lives, so its controller will also remain available for as long as the app lives, just like Redux’s store.

Why is the local controllers approach better?

This approach immediately solves all the problems we encountered in the global store approach:

  • Better encapsulation: Not all the data needs to be visible to all the other components. If you have a component that needs to share some prop with some of its nested children, just put this prop in the component’s controller. If you need some of the data to be visible to all the components for as long as the app lives (application data), then put it in the controller of the root component. Example for such data would be locale, session, meta-data etc’.

  • The controller’s life cycle is now bound to the component’s life cycle: Back to the shopping cart example, we could now put the isCartFull flag inside a component that we’ll callShoppingPage, and at the end of our shopping experience when the ShoppingPage will leave the screen, its controller will also die. When a new ShoppingPage will enter the screen it will receive a fresh new controller (with fresh state) out of the box. No need to worry about cleaning the state before a component enters the screen.

  • Multiple instances support out of the box: If we want to display two shopping pages on the screen simultaneously, god knows why, we don’t need to worry about collisions- every ShoppingPage instance holds a reference to a different controller instance, with different isCartFull flags in their states.

When do we still need a global store?

The cases when a global store is really needed aren’t very common, but they still exist:

  • When we need to share data between components that do not necessarily belong to the same subtree, but the data they share cannot be considered application data: Let’s say we have a Banner component, and we can put banner instances wherever we want inside our app. Now let’s say that when a user clicks on one of the banners, all the banners should change their background image. We don’t want to put bannerBackgroundImage inside the app controller because it’s not an applicative data, so in this rare case we probably really need to create a bannerStore to hold the bannerBackgroundImage prop.

  • When we need to persist some of the component’s state even after the component has left the screen: Most of the time in such cases, we can put the data on a parent that will stay on the screen after the component has left, and we don’t need to use a global store. If we don’t have such a parent, or if the data really doesn’t fit within any of its parents, it’s legit to use a store.

There is nothing new to this approach, it’s just MVC

Controllerim brings back the good old MVC pattern. For some reason, people think that data flow in MVC has to be a two way flow, but it’s not true. This diagram was taken from the wiki page of MVC:

MVC

You see how the data flows in a unidirectional way? Actually, you can replace the “model” with “store”, the “controller” with “dispatcher”, and the arrow that comes out from user with “action”, and you’ll get the exact same diagram of FLUX. The only difference is that Flux tells you to use a single controller (=dispatcher) and a single model (=store) for all the components in your app, and supposedly it should be more scalable.

If you are doing MVC right, it will be much more scalable and easy to manage than using a global store, and Controllerim will surely help you to do MVC right.

What about the other problems mentioned?

Here comes the fun part- Controllerim has almost zero boilerplate, following the flow of the data cannot be more straight-forward, it’s very optimised in terms of performance (using Mobx behind the scenes), and the learning curve is about 15–20 minutes, no kidding: Getting started with Controllerim


Few words about Mobx

I was only talking about Redux, but there is one famous alternative to Redux called Mobx. Mobx is a great state management tool, but keep in mind that it’s only a tool. While Redux is mostly a design pattern (“do this, do that”) and only a tiny bit of a tool, Mobx is 100% a tool, and it comes without any instructions for how to use it. Controllerim is actually built upon Mobx- It takes Mobx and gives it a structure, leaving less space for errors. Controllerim also got rid of some of the rough edges of Mobx, so the result is a very simple to use, straight-forward state management tool, which promotes a better design pattern than Redux.

Conclusion

Today, many people really think that Redux is an inseparable part of React, and many tutorials for getting started with React teach Redux by default. It seems to me that no one stops to think if there is another way, and I found out that it is really hard to convince people to even consider trying out an alternative.

Don’t be fooled by its popularity. Redux is far from being the perfect answer for managing the state of your app in a safe and easy way. I don’t know if Controllerim is the perfect answer, but I really hope that after reading this, you’ll at least open your mind for a new approach.

相关文章