Sometimes I feel itâs quite obvious, but I never saw it somewhere written down. The article is my attempt to show you a way to organize your state with state keys . Iâm using it in my projects, others might already use a similar approach. But nobody advertised it so far.
React + Redux developers tend to use feature folders these days. Most of the time they are coupled to a nested reducer and actions and thus they are less accessible from the outside. They still get exposed yet overlooked. Iâm advertising feature folders as well, but in larger applications one often ends up with a cluttered state.
The cluttered state happens because in feature folders itâs fairly easy to mix up specific and unspecifc domain state. Without thinking in advance about the nested substate in a feature folder, the state gets easily messy.
Consider the following example: You want to show error messages when a request fails (1), loading indicators for asynchronous requests (2) and load more buttons to fetch paginated data from your backend (3). Everything happens in different domains like editing an user or showing lists of messages and authors. Your state might look like the following, where all the things typically nest in domain specific states.
Additionally you face a bunch of duplicated actions and overlapping action types to change your state.
In a rapid development environment it happens quite often. There is no time to plan state structure ahead. There is no time to refactor in favour of abstractions. There is no place to refactor, because you have multiple teams working on feature folders, where every team is relieved to have their owned place.
On the other hand you can clearly see patterns of abstractions. Easy wins. You should take the time to plan your state from the beginning. You should do the refactoring. You should address these topics in an multi team environment.
In the example above you can clearly separate domain specific state from abstract state . You might want a state like the following:
You introduce abstractions for error messages, loading indicators and hrefs to fetch paginated data. The domain specific state (user, messages, authors) stays tidy. The new abstractions (isError, isLoading, nextHref) become domain specific states as well. Thatâs where the attempt to introduce state keys comes into play.
Itâs no magic. Like I said, people may already use it, but nobody documented it so far. State keys use the advantage of keys to allocate substate. I will demonstrate it by the dividing domain specific state from abstract state .
First define your state keys and second divide them into groups.
You can have a constants file for each group.
The constants file for each group is important. It describes a finite number of allocated keys, thus a finite number of substates [C] in a group. Each group itself represents a substate [B] in your global state [A].
Once again in JavaScript syntax with some dummy state:
Now itâs time to implement a reducer + action pair for each group. To keep it simple, I show it only for the isLoading group.
After all you will end up with the following relationship.
It seems like we pair one action with one reducer very strictly. But thatâs not the case. On the one hand you can still keep your reducer accessible for other actions. On the other hand you can use the action in other reducers as well. Keep your actions as global events and donât use them as local commands.
Each group allocates the same payload though. All state keys sharing the same action + reducer pair store the same data model.
Now one could allocate all stateKeys (USER, MESSAGES, AUTHORS) in the group (substate) isLoading. Here is one example how you would indicate that a list of messages is loading:
Since it is a finite number of state keys in each group, the substate is predictable when you follow the constraints of state keys.
Moreover itâs even possible to retrieve the substates easily by their state keys. We are using selectors for those cases nowadays.
Optionally you can even more decouple the substate from the state.
Additionally a static type checker like flow would be another great benefit. One could register all state keys for specific reducer, actions and selectors. It gives one a very predictable substate container.
Maybe the theory looks more difficult than it is. Imagine we would have implemented reducer + action pair for each isLoading, isError and nextHref group. Moreover we would have the dedicated state keys in constant files and additional selectors to retrieve the state by using state keys.
Now the following use case: We want to fetch paginated data from the backend.
The state key abstraction made it easy to deal with all the shown cases for asynchronous requests.
Moreover imagine a button component beneath our list of messages, which could be responsible to fetch paginated data. Once you click the button, the implemented fetchMessages action would get triggered. The button knows about the nextHref to pass it to the fetchMessages action, since its container component retrieves nextHref by using a state key selector getNextHref(state, 'MESSAGES') .
The example shows it only for MESSAGES , but you could easily exchange the state key to AUTHORS and implement a fetchAuthors function. Additionally the pattern scales: Once you want to add a new domain like âCOMMENTSâ, itâs quite simple to add another state key which benefits from the pattern from the beginning.
In our example we only store primitives. But you can apply it for complex objects as well. Imagine a Table component which supports to sort, filter and select. You want to have these states in your global state to keep it accessible from the outside. Now you could register each table component depending on their set of features (select, filter, sort) to different substates (groups) with their state key. Not all tables need to support all features.
Now itâs fairly easy to keep track of different Tables and their substates. You can retrieve everything by using state key selectors.
A static set of state keys describes a finite number of allocated substates. The substate is predictable. On the other hand you may be already used to a dynamic allocation. The following should be familiar to you, especially when you use normalizr :
State keys enable a dynamically allocated yet predictable substate. State keys are used in favesound-redux - a real world SoundCloud Client application. They are located in src/constants .
At the end I want to give you some key takeaways of state keys:
Even though you can apply the pattern without a library, a very good friend of mine already implemented redux-state-keys for you.