loading...

Seriously - do React hooks replace state containers?

yakimych profile image Kyrylo Yakymenko ・6 min read

The question about React hooks replacing state containers has been coming up more and more often in the React community recently.

Nevertheless, the question is not completely out of place. useReducer and useState handle the state management part, while useContext can help with its "global nature".

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.

Simple React app wireframe

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:

Clean component tree

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.

Open 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:

Stateful component

The arrows from state and 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:

Component tree with state sprinkled around

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!

Component tree with context

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:

Component tree

Discussion

pic
Editor guide
Collapse
patroza profile image
Patrick Roza

I think the alternative to prop drilling is to have the presentational components largely for styling and layout while filling them with children and other components by props from a more central location higher up the tree like the page level. (So these components are “open”, like you use the browser provided html5 elements; few attributes, lots of children)

You have the state largely centralized per page and removed most prop drilling from the equation.

State locality becomes clear, low amount of indirection, and there’s a clear approach where state is managed and attached (even with redux I personally don’t like the idea that deeply nested components get “connected”)

You can then instead use context for more shared or global-esque state and reusable behaviors, and avoid the indirection and complexity of state containers.

Collapse
misterhtmlcss profile image
Roger K.

I find what you are saying interesting. I'm just wishing I could see some significant code to represent it. Did you produce a little project anything recently that you'd be able to share that represents current thoughts? Most examples are very terse and I wish that sometimes people happened to have a recent project that someone could review to learn from rather than a few lines of code. Please and thank you

Collapse
patroza profile image
Patrick Roza

I'm preparing a post. Hopefully ready within a week.

Thread Thread
misterhtmlcss profile image
Roger K.

That would be awesome! Thank you. Can I request that the code to review is more than just 4 or 5 lines completely out of context. Would be awesome if there were samples in a post and then a link to a GitHub that relayed a larger more inclusive context to review. That's so so rare and that would be awesome to see especially in the subject we are discussing

Thread Thread
patroza profile image
Patrick Roza

Alright, that's a bit of a tall order for a first time blogger and a larger topic ;-)
However I started a post series, the first one just landed: dev.to/patroza/component-vs-prop-d...

I will try to expand on it in the next one, i'm considering to use your "hypothetical" application as a basis.

Collapse
avkonst profile image
Andrey

React.useState + useContext combo is a possible way to lift the state up, but these tools out of the box are not optimal - two issues are 1) code complexity as discussed above and 2) bad performance on frequent state changes. However, there is a supercharged React.useState wrapper, called Hookstate github.com/avkonst/hookstate, which makes lifting the state up easy and efficient. It made Redux dead for me and for at least few other people, because it is simpler and faster. Disclaimer: I am an author of the project.

Collapse
goncalohit profile image
Gonçalo Santos

Finally someone with good sense, many people just follow the hype train!

Collapse
loujaybee profile image
Lou — Cloud Engineer

This is really well written Kyrylo! Nice examples and I really like the diagrams! 👏 Thank you.

Collapse
yakimych profile image
Kyrylo Yakymenko Author

Thanks, nice to hear!