If you're working with complex and interconnected state in your React application, Recoil's atomFamily function may be a valuable tool in your state management arsenal. atomFamily allows you to create a family of atoms that share the same properties but have unique keys, making it easy to manage multiple instances of the same state.
In this article, we will discuss the benefits and best practices of using atomFamily in React. We will also provide an example implementation. By the end, you will be prepared to use atomFamily effectively in your own projects.
So let's get started!
Understanding “atomFamily” implementation and use case
What is an atomFamily?
First, an atom is a piece of state such as a useState() in React. But an atom is global; it is defined outside of react (regardless of whether the components that use the atom are mounted or not). It is define like this :
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
An atomFamily is a group of related atom that share the same structure and behavior. In React, it would be like if useState depended on a parameter : useState(id)().
What does the implementation of an atomFamily look like?
In this article, we will use Recoil's AtomFamily feature to implement a render-proof todo list example. The implementation is very similar in Jotai, another state management library.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Typing of the atomFamily is atomFamily<TypeOfYourAtom, TypeOfTheId> (In our case Todo is type Todo = { text: string; isDone: boolean; })
key is a unique string to identify your store
default is the default value of the member of your family. All values exist! So calling useRecoilState(todoAtomFamily(anyId)) will work and return either the default or the value if it has been defined !
This is also why we need to keep track of which ids are being used in a basic array. You can’t access the lists of used atoms from Recoil.
Then we define functions to update our store. These functions need to update both the todoAtomFamily and the todoUsedIdsAtom.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The syntax of useRecoilCallback is similar to a useCallback but it exposes some util functions to update the store (reset, set, …) : useRecoilCallback(({set, reset, ...}) => (callbackParams)=> { /* The core of the function */}, [depArray])
Our store is now ready to be used:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Now if you want to make it interactive, you can access the state of the atoms with useRecoilState(todoAtomFamily(id)) which works exactly like a useState. If you want to use only the value you can use useRecoilValue and if you want only the setter you can use useSetRecoilState (This is an example for todos but you can have any list of interactive elements).
Evaluating performance benefits for your app
If we display the re-renders we can see that toggling a todo does not cause a re-render in the other todos. One of the major benefits of using AtomFamily is its ability to improve performance by minimizing unnecessary re-renders. Since each item in the family depends only on its own atom, it won't re-render when others change. This is particularly important in complex applications, such as drawing apps, where rendering can be expensive and time-consuming. In these cases, using AtomFamily can significantly improve performance and user experience.
✨ TIP ✨
You can show the renders on the web by clicking "Highlight updates when
components render" in the React DevTools browser extension. (chrome
or
firefox)
But do you need an atomFamily?
So, you only need an atomFamily if you have a list of interactive item and you care about performance.
Comparison with Zustand
Zustand is a super popular state management library. It has slowly become a standard for most React / React Native projects. Here’s the doc to better understand Zustand. Let’s see how we would implement a similar feature with the same performance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In Zustand, we define the function to edit the store (addTodo and toggleIsDone) inside the store
The page implementation is similar to that in Recoil.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The biggest difference is in the implementation of the list and item:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
As you can see, achieving a render-proof implementation can be more challenging compared to Recoil's atomFamily. Zustand requires the use of memoized selectors and an equals function (also memoized) to determined if the selected states are equal or not, which adds complexity to the implementation.
So, while it is possible to achieve similar render performance with other state management libraries like Zustand, Recoil's AtomFamily provides a more intuitive approach that can lead to better performance in the long run. With AtomFamily, developers are encouraged to break down complex states into smaller, independent atoms, which can lead to more efficient rendering and easier debugging. Additionally, Recoil has some built-in dev tools to make it easy to monitor and optimize performance.
Conclusion
In conclusion, atomFamily is a powerful feature in Recoil that allows you to manage a group of related atoms with shared structure and behavior. It is particularly useful when dealing with a list of interactive items and performance optimization is crucial.
The main advantage of using atomFamily in Recoil is its ability to minimize unnecessary re-renders, improving performance in complex applications. This is achieved by creating independent atoms for each item in the family, which only re-render when their respective atom changes. Additionally, Recoil provides a more streamlined approach to breaking down complex states and offers built-in dev tools for performance monitoring and optimization.
While Zustand offers a simpler store definition and similar performance optimization capabilities, Recoil's atomFamily presents a more intuitive approach to state management, especially for developers who care about performance and wish to work with lists of interactive items. Ultimately, both Recoil and Zustand have their merits, and the choice between them will depend on your specific requirements and personal preferences.