Photo by Jonathan Rados on Unsplash

Three Painful State Patterns That Can Be Solved With a Custom Dynamic State Hook

David Hammond
9 min readJan 1, 2021

After working with React for about three years now, I was one of the developers who went through the transition of working with React professionally, both pre-hook and post-hook era. This experience is a different perspective than those coming into React, now, in the post-hook era. After spending a lot of time converting class components into functional components, and writing functional components from scratch, I have come across three painful patterns I ran across consistently. I want to emphasize, I am not saying these patterns are bad, in fact, these are patterns I use many times on a daily basis. They work, and I wouldn't even say they are a bad practice. There is just something about these patterns as a developer, I just don’t care for them. They seem to take a little too much thought, and/or overhead to execute them, and when you can make something simpler you always should. That is the goal of this article, is to observe three patterns I have noticed that I encountered on a frequent basis in React specifically related to state management, and to demonstrate how they became less painful, using a custom hook, which implements dynamic state management.

Problem Pattern #1: Complex Functional Component State

Complex components can have a lot of logic to support certain use cases. For a complex component, it would not be difficult to see something like the below in regards to the state.

There is nothing wrong with this, but can we please discuss some of the pitfalls. First off, I need to remember many function names to work with this. It would be very easy to make a typo. In fact, when I encounter a complex component and decomposition does not seem to be the right choice, I find myself missing this.setState() from class components. This is actually an argument I have heard made against using functional components for large stateful components, from a purely organizational point of view. For me, this is a major pain point. I don’t want to remember all these function names, and imagine I am coming to this component to fix a bug and I have never worked on it. Let’s see what would happen if we did something similar but instead of spreading out state into many useState calls, what if we merged into one, and used an object instead.

Problem Pattern #2: State Represented As An Object

Let’s take the previous example and take a different route. We will take all the separated items of state and make an object to manage.

When we want to update the state, at least now we can remember one function name, setComponentState but now we have another tradeoff to weigh. We need to copy the object every time we want to update.

Again, this is not a bad pattern, but still not great. I don't want to have to copy the object every time, and overall I prefer the first method, where I had state separated into its own entities, but I like the concept I encountered in the second method, where I just had to invoke one function to update state. It doesn’t seem like we have really seen the balance between both those yet.

Problem Pattern #3: Refactoring Class Components

Problem pattern number three, I encountered when refactoring class components into functional components. One of the first ways I started to become more familiar with hooks when they were first introduced, was to take an application where all state was managed as class components and using the lifecycle hooks provided by class components, and convert the entire application to use functional components with the new hooks API. I very quickly noticed that when I was doing this, I encountered the below quite often.

Now, we might decide to translate this state object into:

Or we may try and change it into this:

See where I am headed here? The solution to refactoring class components brings about problem patterns 1 and 2. So in theory not as much a pattern, but you get what I mean here. So the bottom line is, I want something like this.setState() where I can always use the same setter. At the same time, I don't want to manage all my state simply as objects, I still want individual items of state. I also want one more thing we have not discussed, but was maybe assumed if you are familiar with the setState API, which is batching together my updates. I don’t want to have a function like this:

I want to see something more similar to our old friend:

That should set the scene for the problems I was seeing and the problems I was trying to solve when I decided to create a dynamic state management tool.

What is Dynamic State Management?

Let’s discuss this cryptic and important-sounding phrase of dynamic state management. It's nothing more than a repeatable way to create pieces of state, and manage their state setters, using a basic API. In a sense, we want to create a factory for generating state that can be used in the component and in addition, state setters to update these values. First off, how will we know what values the user wants to put on the state in the first place? We need to take in some input from the user. In addition, how we will expose the “state” version of the value back to the user, and how will they know how to reference it. In this case, I have found the best input is an object, where the keys represent the “variable names” which will be returned and can be used in the component as stateful variables. Before we go any further with this discussion we need to address the elephant in the room.

Isn’t This Inherently Wrong?

Based on your familiarity with hooks, you may have already been shaking your head saying you cannot do this. If you use eslint and have ever written a useState hook inside of a for loop, you will see the following message:

React Hook “useState” may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.

If we visit the rules of hooks page for React we also see some documentation that should definitely make us at least think twice about what I am suggesting here.

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

Oof. So is this bad practice? What are the keys to making hooks work correctly?

By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

Hooks MUST be called in the same order, this is the key here. It is explored more in-depth in this Netlify article, which I found extremely insightful on understanding how hooks work. Remember, hooks are not magic they are just arrays. So the key is when we supply the starting state to this dynamic state hook, we must never mutate the value that is provided. The only mutations that can occur must occur through the state setter function exposed by the custom hook. See the below example to see a hypothetical bug.

In the case that this data value is not mutated the hooks are called in the correct order and everything works as expected. See the below code snippets, and notice it is impossible for the order to be different as we refactor these unless we mutate the input in the final example:

Can be refactored to:

Now, still nothing wrong, but not very useful as the variables are only available in the scope of function but still works.

Again, not very useful, because of scoping but this is still the same thing. When we begin to break the rules of hooks is when we do something like this below:

Moving Forward With Our Example

Now that we have discussed this elephant in the room, let’s come back to the representation of the initial state. Can we use an object? Let’s ponder the implementation. We would need access to the keys to understand what the returning value would be on the object we return and we need the value associated with it to understand what the initial state would be. Sounds like we need Object.entries . According to MDN documentation:

The Object.entries() method returns an array of a given object's own enumerable string-keyed property [key, value] pairs, in the same order as that provided by a for...in loop. (The only important difference is that a for...in loop enumerates properties in the prototype chain as well).

If we visit the MDN documentation on for…in we find the following note:

Note: for...in should not be used to iterate over an Array where the index order is important.

So I think based on some of the objections we covered related to the rule of hooks, this is not the best route to go, even though it may expose a better API to the developer. So let’s take a book from the Map data structure in Javascript. To create a Map that already has populated values, we can pass an array of arrays, where the internal arrays contain key-value pairs. See the MDN documentation here.

const myMap = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
])

We will follow a similar pattern for our initial state. Index position 0 represents key names, and index position 1 represents initial values. Going with the same example throughout the article, calling this custom hook would look something like this:

So we would expect to have access to state.columns and initially, this will be an empty array, and state.isLoading which would start as true, etc. To update this state, you would simply invoke the setState method and pass in the key name and the value.

Need to update more than one property?

Need to update using the previous state? Because this idea is simply an abstraction and not some fancy magic, just pass the function and natively React’s state setting function returned from the managed useState state call already knows how to handle this:

How does it work? You can look through the full code here. And you can see a basic Todo application using the package live here. Basically, you look at the first position of the incoming array and know that for the values returned to the user, they expect an object with those keys and the values should be the “state” version of the initial value, which would be index position 1 of the starting array. While looping over the arrays, you keep track of the state value, and the state setter returned by the useState call. I chose to use a map to track these. Separately, you have an object with the key names passed in on index position 0 of the starting arrays, but the values are the “state” representations we have created for the consumer. From there, all it takes is a small wrapper function, which exposes both accepting single values to update and an array of values to update all at once. It takes a key or set of keys, that you want to update, and it looks up the setter which is stored in the map and invokes the setter with the new value, thus updating the “state” representation of what we returned previously and updating the component as normal. If you want to check out the npm page for more information feel free:

Conclusion

There is nothing wrong with the patterns I have shown here. In fact, many of these I see on a daily basis in my professional life. This particular state management helper tool was developed to encourage quicker, easier, and more uniform coding practices. It eliminates the need to remember different function names within a component, nevermind throughout your application related to local state management. It also helps clean up functions where multiple pieces of state need to be updated at once in functional components. I have found this to be an incredibly useful tool, which I have been taking advantage of on all my personal React projects, and wanted to share. Feel free to fork and extend the functionality as well.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

David Hammond
David Hammond

Written by David Hammond

Senior software engineer always trying to learn something new. Spend the majority of my time working with React, Angular, and developing cloud solutions.

No responses yet

Write a response