I have been writing React applications now for just over a year, and in that time, my approach to writing those apps has changed quite a bit. At first, I was just using the basics — write a component for a button, write a component for some text, write a component for a modal. At the heart of it, this is all that React was really intended for anyway.
Then I started learning about state and the way to make UIs adapt as the user interacted with it. We wrote large class based components that implemented whole pages with a bunch of state that was stored when the user would fill out information. This was great, but it started getting us into some trouble. Our React components, which used to be elegant, were turning into large messy ES6 classes full of spaghetti code.
One of the things that I have learned over my career is that size does matter. The larger a section of code is, the more paths there will be through it, and hence the more complicated it gets. Complexity is the thing that will kill any project, no matter how hard you strive to engineer it correctly. One of my classes at MIT covered this topic, talking about how corporations all around the world would spend millions or even billions of dollars on software projects that eventually had to be scrapped due to complexity.
So how did we proceed? We changed the way that we wrote our components. For one thing, we stopped using class-based components. Functional components are generally much simpler because at the end of the day they are just functions that get called when something needs to be rendered. We also broke large components up into collections of smaller components.
When we had gotten our basic component down to around 100 lines of code (and really, you should try to keep every component this small), things became a lot easier. We weren’t done, though. We still had business logic that was still clogging up some of the components. That’s when we turned to Redux.
I’ve talked to a number of React developers who still aren’t really familiar with Redux. React has several ways of storing state, from simple hooks like useState to more global state like useContext. In a lot of ways, this is sufficient for teams. Redux isn’t so much trying to replace these as it is trying to shift state management away from components. Let the components focus on presentation and let Redux focus on determining the state of what is rendered.
Comparing Redux to useState or useContext however, doesn’t give the full picture. Redux is largely about actions. That is, when a user pushes a button on a page, an action is fired off that causes certain operations to occur, some state to get updated, and then ultimately an update to the view. This is the basic lifecycle of an application — user pushes button, application does some work, application updates view/state.
We started with a large flat Redux state with all the different variables we were tracking. This worked ok, but one of the great things about Redux is that you can combine individual groups of state into the overall state, and so we broke down our state based on areas of functionality. For example, if we were writing a weather app, one group could be about the actual weather data, another group could be about which view we were looking at (hour by hour, day by day, etc), and another could be about user preferences on how the weather data is displayed.
Moving the business logic into Redux away from the components made things a lot better. Now our components simply used Redux selectors to pull the state we needed and the rest was just rendering based on that state. One thing that did start happening, however, was a lot of useEffect hooks creeping in. These are little hooks that watch for Redux state updates and sometimes trigger other actions.
An example of this is when a user submits some data for validation. We fire off an action that validates the data, running through a series of checks. When we are done, we can set a redux state variable that says the data is valid. Other components that are meant to update when the data is validated can watch this variable with a useEffect and then update their own local state which will lead to a re-render. I’m still working on a better pattern for this, because I really don’t want components doing much else other than rendering HTML out.
Another thing that I found with Redux is that it can turn into a dumping ground for simple global state. That is, you fire off actions that do nothing more than set a Redux state variable. This is actually not a good thing, and recently I’ve discovered Recoil, a newer framework that has come from Facebook that tries to better address global state.
The problem with something like useContext is that each time you use it, you are creating a context high up at the level where the context provider lives. When you change the context, it causes a cascade of render updates down from the provider. Recoil addresses this by using something it calls atoms. These atoms aren’t tied into the React component tree like the contexts are, so updating the value of an atom doesn’t require the entire tree to re-render.
What’s even better is that you can use the values of atoms to compute a derived state (called a selector) that is automatically updated when the atom changes. For example, you may a price displayed as a total in one place and then have the price broken down in various ways in other parts of the page. By using Recoil selectors, you can just store the total price once and have selectors that compute the breakdown of the price in various ways. Update the total, the others update automatically.
The great thing about React is that it is continually evolving, and I’m evolving my methods along with it. My applications are working so much better than when I started, and although things my not be ideal everywhere, I’ve been able to focus a lot more on things like CSS and delivering value and not so much on keeping my components maintainable. I can’t wait to see where we go from here.