Recoil Patterns: Hierarchic & Separation
This article will discuss practical patterns in Recoil.
It’s an advance topic that goes beyond Recoil basics, so we won’t spend time describing Recoil or its fundamentals concepts.
If you’re not familiar with Recoil I suggest starting with the following sources:
* Official Recoil YouTube
* Recoil documentation
This article is brought to you by WeKnow and represents insights gained during architectural efforts while moving from REDUX to Recoil.
We’ll discuss the structural concept for keeping the global state’s unit autonomous and decoupled, but combined with a higher level of abstraction and composition.
Different components may have to interact with different portions of the state data. It could be a simple control that is responsible for presenting an atomic unit of data (such as date, rank, name, etc.) or a detailed view that might need to aggregate a wider portion of the state (summary, data-grid, etc.).
This article will discuss ways of serving the needs of both cases using structured patterns.
A code sample for this article is available on GitHub.
It uses Next.js (SSR) with Typescript and Styled-Components.Under Styled-Component, it uses WeKnow’s convention that uses the component name with the suffix “Raw” (<Component>Raw.tsx) for the actual JSX, wrapped with a Styled-Component (Component.ts).
If you aren’t familiar with Next.js,
executing the code done via npm run dev.The code sample implements the scenario in the following diagram.
I try to make it complex enough to represent a real-life scenario, but not too complicated (I hope ☺)
These are the goals that we are trying to achieve with this pattern:
* Separation of Concerns: Each component should be familiar only with the data portion that it needs. It doesn’t need to know about other portions of the data.
* DRY: Each data unit should be declared only once.
* Composition: complex states should represent a combination of atomic states.
* Change Tracking: Central tracking of individual states currently exists in the state storage (under specific segmentation).
* Optimize rendering: Changes in one component shouldn’t lead to the rendering of unrelated components.
Let’s start with our state.
Atomic State:
The atomic state represents:
* Order’s properties: Product ID, Color, Size, Count.
* Review’s properties: Product ID, Reviewer, Star Rating.
To handle multiple orders/review instances, while keeping each instance’s data separate from the others, we’ll use Recoil’s atomFamily instead of atom.
The following code snippet presents the state’s product-id unit:
atomFamily represents a family of atoms (with a common meaning).
Each member of the family can be accessed via a key.
The key can be simple (string, number, etc.) or complex (an object).
In the above case we used a custom complex key which represents the journey-type and ID (of order or review).
This complex key pattern enables the reuse of the same state unit in different contexts. For example, you can use product id both in review and in order, while keeping the two perfectly isolated .
The following code snippet shows the IRecoilId structure.
* extends Readonly<Record<string, string>> is required for Recoil’s family key (when using a complex type)
Tracking:
The next pattern is almost mandatory when using the Recoil family.
At the time of writing this article, Recoil don’t yet have an operator that can fetch all members of a family. This means that you need to maintain a state of all family members in order to be able to get all members at a later time.
The following code snippet presents this concept.
The idea behind this pattern is, whenever you add a new family member (under a specific journey) you should add its ID to the list.
The tracking list’s interaction could be direct by targeting the list atom, or indirect by, for example, removing an item from a list when resetting a higher level abstraction (more about this a bit later).
So now we know how to define our atomic data unit and we are familiar with the idea of a tracking list. So far not so different from what you can see in any other tutorial.
Composition and Encapsulation
No one likes to continuously grab the same pieces of state again and again in order to handle hierarchic data on different components (or so I assume).
Having a higher level of abstraction over a group of atoms will serve us in multiple ways:
* It’s simpler and easier to use — you don’t have to figure out which atoms are related to a type.
* Better maintainability characteristics, i.e. consider adding a new optional state-related field to an order or a review. Abstraction lets you handle it in a single place rather than tracking all code parts which handle it in different locations.
* Composition — being able to preform aggregative operations that affect multiple atoms by invoking a single command (for example, reset).
The following code snippet illustrates this idea:
This selector gets a parameter which represents the specific order ID in a journey type.
Targeting get accessor will return an aggregate result of IOrder from relevant atoms, which together present the order details.
The actual aggregation is done by waitForAll, which waits for the result of multiple states (very useful for potentially async operations).
The following code snippet shows how to consume it.
It can be consume in a single line rather than having to grab each atom separately.
Set accessor looks at the payload to identify whether it’s empty, and then issues a reset request.
It uses typescript’s guard concept.
The following code snippet shows the implementation of guardRecoilDefaultValue:
This is the snippet part dealing with the reset:
A non-empty payload will result in setting all of the order-related atoms.
Both payloads affect the tracking. A non-empty payload will add the new order ID to the tracking, while an empty payload will remove it.
This side-effect handling is one of the benefits of using an abstraction.
Usage Patterns
In order to avoid unnecessary rendering I encourage you to split your code into sub components, each dealing with a specific state portion.
It’s important that the component itself (not its parent) does the actual useRecoil… statement.
The following code snippet presents the idea.
Sub component:
Parent Component:
Summary
Recoil is a promising technology, but technology alone, no matter how advanced or perfect, has the potential to lead you down problematic paths if you don’t put in the effort to architect your solution properly, according to your business and technology goals.
As is the case in any other pattern, it’s only good as long as it serve your goals.
You can see the source code attached to this article on GitHub to get a deeper look at the actual implementation.