The question about React hooks replacing state containers has been coming up more and more often in the React community recently.
Dan Abramov@NgozikaDanny Hooks by themselves are an alternative way to write and reuse logic between components. So they’re not any more “alternative” to Redux than classes. They’re just a way to write and compose code.
The question of whether you need Redux seems unrelated to Hooks to me.11:16 AM - 16 Apr 2019
Moreover, a lot of advice floating around sounds as follows:
- start by putting state in your components.
- whenever a piece of state needs to be shared, lift state up.
- if prop-drilling becomes an issue, throw it onto context for ease of "global" access.
Armed with this advice, and eager to test it out in practice, let's go ahead and build a React app!
The initial specification from the client looks simple enough: a page with a header (containing a Menu and some user information), a footer, and a sortable/pageable/filterable grid in the main content area.
Looks really easy — following the advice above, let's put the state for the grid content and paging close to the grid itself. We also have some state describing what column the grid is sorted by, as well as what filter values have been chosen for each column. So far so good! Development is going really fast and we can quickly iterate on our solution. The component tree looks nice and clean:
At some point we add a toolbar with a settings button, that would open a "settings" dialog and allow to set the number of items per page. Since this state is currently in the "paginator" component, we do not have access to it from the "settings" dialog. No problem, we lift the pagination state one level up and pass it down to both the "paginator", the grid and the settings dialog.
The first Demo to the client is a success! We get some feedback and they would like some improvements — they want to be able to export the grid content to PDF, so we add a new button to the toolbar — it would open a confirmation dialog with some information about the total number of rows to be exported, so let's lift this piece of state up from the grid too, and pass it down to both — the grid itself, as well as the "report generation" dialog. Manageable so far, but getting messy. Time to document the structure. A stateful component with a couple of
useState hooks and a
useEffect hook would look something like this:
The arrows from
setState go to and from
JSX, since we would probably use and set state from there. The same applies to props.
useEffect closes over props too and has access to state, so let's add those arrows too. We end up with a lot of arrows, but everything is nicely encapsulated inside the component, so let's update our Component Tree diagram:
A slack message comes from our UX team — they've convened and decided to have the settings icon down in the footer instead. We've already done some refactoring and lifted the "page size" portion of state up once, but now it would end up right in the root component, since it's the only common ancestor of the components that need it. Prop drilling suddenly becomes real, so it seems like it's time to throw it onto
context and access it directly from the components that care. This would add a few more arrows to our component-tree-structure-diagram, but we implement it and move on.
A couple of sprints later an email from the client rattles in our inbox — they've tried the current version of our page and would like some changes:
- it should be possible to choose what columns are to be exported to the PDF report.
- the report should respect the filters chosen in the view.
- the name of the user exporting the report should be displayed in the dialog (remember how this information is now local state in the header?)
Somewhere around this point is where we start slightly hating our life — things are getting really tough to keep track of! We'll have to move some more chunks of state around, namely to lift column-state and filter-state up from the grid, and lift user state up from the header to the root component.
Some of the excitement we felt at the start of the project has now worn off, and the rate of progress has considerably slowed down. But things have not gotten completely out of hand, have they? Let's buckle-up and get to it!
We're diligently following the recipe by lifting state up or putting stuff into context — which adds more and more arrows to our component tree diagram. We discover that the view becomes slow and laggy, so we fire up our profiler and discover that large portions of our component tree get unnecessarily re-rendered too often, so we take a step back and analyze the situation, determine which parts of the view can be memoized and wrap them in
React.memo. In some cases we get immediate improvements, but others, however, don't seem to be affected by the change since some lambda functions are passed as props. We solve it with the help of
useCallback instead. It feels a bit like a chain reaction: solutions to our previous problems cause new problems, and solutions to those require more solutions, and so on... No matter what, we discover that every one of those problems is solvable as long as we dig deep enough into how React works, so there is no point in complaining. We're done with most of the functionality for now, so let's ship!
Before we do, however, we get one more request from the client that just finished testing the latest version and found the "settings" icon to be difficult to find down in the footer. Since they use it often, they would prefer it to be next to the grid, as in the initial version. Hmm... let's try to remember where we put the state for that one... Right, it ended up on context, since we pulled the components apart after some feedback from the UX team a while ago. Now it's going to be close to the grid... so shall we remove it from the context and put it back into local state close to the components? Nah, just leave it as is, you never know when we're going to have to move it again 😬
A few months later a new dev comes to work on the project and is given the task of adding information about the current page and page size to the PDF report dialog.
— New dev (after some detective work): There is one thing I can't quite wrap my head around... How come pageSize is placed in context?
— Old dev: Can't remember how it ended up there, the dev who did it doesn't work here anymore, but at some point it was probably more convenient, since it was used from the footer or sidebar or something.
— New dev: Really? Isn't it ridiculous that we have to do so much refactoring every time a UI element is moved to a different place?
— Old dev: At least we managed to avoid pulling in a state container! 🤷♀️
Somehow this reminds me of the term "coding by coincidence". Stuff ends up the way it is because "it just happened this way".
This approach contrasts starkly to what software architecture is all about — having a flexible way of adapting to changing requirements.
Having said that, not every project requires a flexible architecture. If the application is really small, or if it rarely has to change, structuring and architecting it would not be a reasonable time investment.
Hopefully this little (totally made up) story helps shed some light on the question whether hooks replace a state container. The (slightly elusive) answer is — "with hooks, classes or other mechanisms that React provides, it is indeed possible to achieve most of what a state container offers", but more often than not, the application structure is going to look much more messy than in the image below.
Let's see this again in slow motion: