Recoil is React’s best solution so far for global state management. It’s lightweight, simple and performant approach to communicating between components provides a better experience for both the user and the developer than previous libraries like Redux or RxJS. However, the project is very new, and lacks the years of community involvement to produce standard best practices for setting up your project with an eye towards scalability. What follows here is what I’ve found to work best for a large React Native and React Native Web hybrid application.
Problems With Prevailing Structure
Most of the Recoil examples I’ve seen others produce on the web create a structure similar to this:
A single directory is created under
/src/recoil which is then split into
/selectors. This works, but there’s a fundamental problem here that isn’t apparent when you first begin working with Recoil. Let me start with an example of why this isn’t ideal.
Here, we have the
/selectors folders separating two files:
exampleSelector.ts. Each of these defines a single atom and a single selector. The
/components/Main.tsx file shows how to access the data from both files.
One of the premises of Recoil is to allow better performance by cutting up larger data structures into its component parts, hence the name “atom”. Each atom is subscribed to individually by components that reference it through the hooks
useRecoilState, and others. So, when these pieces of state update, components attached to them automatically rerender.
This forces us to keep our atoms simple and small, so that multiple components can refer to the just the pieces of data they need, rather than larger data structures that can have changes occur that not all of our components care about, thus forcing our components to rerender when they don’t need to.
How does this relate to our folder structure above? It means that we’re going to have a lot of atoms in a large application. Any time we have two components accessing a single atom, but caring about two different pieces of data inside that atom, we’re going to have to split it up to avoid the two components rerendering whenever either of the pieces of data change.
Problems Naming Files and Atoms
Another problem I see across many Recoil projects is that the files and data structures are named poorly. Frequently, atoms are named with a trailing “State” suffix, and selectors are named as “Value”. For instance, our “example” atom above would end up in files named “exampleState.ts” and its selector would be “exampleValue.ts”. This gets extremely confusing once you start mixing your Recoil atoms with ephemeral local component state. “State” is an overloaded term in React, and it needs to be avoided. “exampleState.ts” doesn’t really have “state” in it — that’s what you get back from calls to React’s internal
useState() hook. What is a “value”? I can’t think of a more generic term to obfuscate what we’re really getting back from that file.
Managing Your Exports
The exports from your atom and selector files need to be deliberately structured to prevent confusion as the data grows and mutates, something which is inevitable in a living project. Atoms are the heart of Recoil, and so we should make them our default exports from the “*Atom.ts” files.
Structuring the selector file exports are an opportunity to save ourselves a lot of work later. You’re going to have a lot of selectors, whether you’re integrating async fetch calls or just need to reshape what you want back from your atom, these selectors are going to multiply later on, and you want to minimize necessary changes to your code when you add, modify or remove them. Areas we care about are:
- Your import statements for atom and selector files.
- How your changes look in source control and pull requests.
- Developer experience.
In our earlier CodeSandbox example, take a look at “Third.tsx”. This component utilizes three separate forms of the “example” atom — one through a direct read of the atom’s value, and the other two through selectors that pad the value with different brackets. The end result’s import statements look like this:
All three files imported here are using their default exports, so we have three import statements. You can also see the
const naming here is a little contrived. For the result of the
exampleState, we are dropping the “State” and just going with “example”. We can’t do the same with the selectors, as the first one is going to collide with “example”, so we compromise on a shortened “Val” instead for both selectors.
From a source control and developer experience standpoint, this is okay, but not great. The file naming alone is going to make it difficult for other engineers to understand.
This may seem like a contrived example. It’s not. I see this often in Recoil projects, especially from developers working with Recoil for the first time.
So How Do We Fix It?
The major issues we need to fix are:
- The files are named in confusing ways.
- The folder structure doesn’t reveal anything useful.
- We have too many imports.
- We don’t know what we’re getting back from the calls to
Better Naming Convention
For #1 above, let’s do away with “State” and “Value” suffixes. These are both overloaded and ambiguous. We’ll replace them with the more precise “Atom” and “with<something>”. The latter “with” more accurately describes what you’re getting from a selector than “Value” which you have to mentally unpack to understand you’re mutating the atom. “With” implies an extra step, which is what we’re doing when we use a selector. The “<something>” is the description of how you’re modifying the base get or set operation. In my example, I’m either adding brackets or parentheses, so those values will translate to files named “withBrackets.ts” or “withParens.ts”.
Refactor Folder Structure
Splitting atoms and selectors into their own folders is a grave error. This is basing your folder tree off of implementation details; instead, let’s make our folders mimic the internal structure of Recoil and base everything off of the atom of data we’re interested in.
The difference between an atom and a selector is that the atom contains the raw getter and setter for the data, and a selector does some kind of work before it acts on any get or set operation. However, they are working with the same data! Whichever one we use, we’re interested in the same piece of data; the same atom. So, we’ll remove the
/selectors folders entirely, moving everything up a level right under
/recoil, we’ll create a new folder
/recoil/example, placing both our atom and selector files within it.
This structure leaves us with the option to import however we like, but also makes it much easier to decide where our unit test should be placed, either in
If you later want to extract a particular atom of data into a separate project or package, doing so is as easy as copying the single folder somewhere else, taking with it all related files.
Rather than having to duplicate every atom and selector import statement with a long path, we’ll use a dictionary pattern to collect all objects related to a single atom inside the atom’s parent file. For my example, I’ve created
/recoil/example/index.ts and the file looks like this:
With the above, we’re free to simplify our imports now with a single statement rather than three separate ones:
You’re free to use the longer names of
exampleWithParens for extra clarity if you need to. I’ve opted to drop the “With” for brevity. There’s just a lot less typing here and with it, less code to read and understand.
Inside the selector files, we’ll take our folder name and our selector file name and concatenate them together in camel case to get the name of our default export and its
key value. This ensures we don’t have key collision issues, as you should never end up with two atoms named the same. Here, the object names I end up with are
Here is our completed sample project, restructured as described above:
We’ve checked all the boxes that concerned us earlier:
- Import statements are terse and flexible.
- All files are separated, so pull requests will be surgical, and the file and folder naming makes understanding what each file does easy to understand just by looking at the paths that are shown in most git command outputs.
- The developer experience is enhanced through better naming and colocation of files. Each file is available for import either directly (`/recoil/example/withBrackets.ts`) or through our dictionary (`/recoil/example`) index file. We also get the name of the atom file in our IDE instead of the generic, context-destroying “index.ts” that can show up when using the dictionary pattern.
A further step I could take here is to split the “example” atom into two different parts “name” and “value”, each with its own discrete atom folder and files. That would improve performance whenever one or the other was changed, as that would have each component subscribe only to the atom of data it needs, but the cost would be decoupling the data. I’ll address Recoil performance patterns in another post, and let this one focus on naming and structure patterns.
Recoil is looking like Facebook’s official answer to React’s global state handling debate. Learning it is going to be necessary for React developers over the next few years as it begins to add more features and supplant more complex or less performant solutions. With the structure described here mimicking the philosophy of the library, you can begin using Recoil in projects of any size and know they’re going to scale well.
I’ll be looking at more aspects of the Recoil library and how it changes the global state landscape in future posts.