How to Migrate a Nested React Context Tree to Recoil: A Step-by-Step Guide

Neelesh RoyNeelesh Roy
3 min read

Consider an application with multiple context providers:

<UserProvider>
  <ThemeProvider>
    <SettingsProvider>
      <SearchProvider>
        <App />
      </SearchProvider>
    </SettingsProvider>
  </ThemeProvider>
</UserProvider>

This seems small right?

No.

Imagine a complex enterprise frontend. Given a plethora of features shipped everyday, the context provider tree can easily jump 40-50 levels deep.

Now, One of the things we can do is to create some sort of array-based solution

// An Array of contexts
const Contexts = Array.from({ length: 60 }, (_, i) => createContext(`Context ${i + 1}`));

// Create providers from the array of contexts
const Providers = Contexts.map((Context, index) => {
  const Provider = ({ children }: ProviderProps) => {
    return (
      <Context.Provider value={`Value from Context ${index + 1}`}>
        {children}
      </Context.Provider>
    );
  };
  return Provider;
});

// Creating a single provider component that nests all the providers
const DeepContentProvider = ({ children }: ProviderProps) => {
  return Providers.reduce((acc, Provider) => <Provider>{acc}</Provider>, children);
};

// Example consumer component that uses the deepest context
const DeepConsumer = () => {
  const contextValues = Contexts.map((Context, index) => useContext(Context));
  return (
    <div>
      {contextValues.map((value, index) => (
        <p key={index}>{value}</p>
      ))}
    </div>
  );
};

const App = () => {
  return (
    <DeepContentProvider>
      <DeepConsumer />
    </DeepContentProvider>
  );
};

If we have this solution then,

Why Recoil? Whatโ€™s the problem?

  1. Eliminates the need for a deeply nested context tree, making the code easier to maintain and more efficient

  2. A deeply nested provider structure like this can lead to performance issues and is difficult to debug. Recoil offers fine-grained state management without the need for complex provider nesting.

const DeepContentProvider = ({ children }: ProviderProps) => {
  return Providers.reduce((acc, Provider) => <Provider>{acc}</Provider>, children);
};
  1. Allows components to subscribe to specific pieces of state without needing to access all contexts, which improves performance and reduces unnecessary re-renders.
const DeepConsumer = () => {
  const contextValues = Contexts.map((Context, index) => useContext(Context));

HOW??!

  1. Create atoms for each state

     const atoms = Array.from({ length: 60 }, (_, i) =>
       atom({
         key: `atom${i + 1}`,
         default: `Value from Atom ${i + 1}`,
       })
     );
    
  2. Add the ONE AND ONLY ONE Provider

     const App = () => {
       return (
         <RecoilRoot>
           <DeepConsumer />
         </RecoilRoot>
       );
     };
    
  3. Subscribed and use

     const DeepConsumer = () => {
       return (
         <div>
           {atoms.map((atomState, index) => {
             const [value] = useRecoilState(atomState);
             return <p key={index}>{value}</p>;
           })}
         </div>
       );
     };
    

Better right?

We have achieve the goal of removing the provider tree with performance benefits.

But,

We can improve!

  1. Dynamically create the atoms when required.

  2. Use useRecoilValue for read-only access, which reduces unnecessary state setters

  3. Use Atom Families instead of atoms, which reduces the need for dynamically creating separate atoms and simplifies the management of similar state pieces. Therefore, improving point 1 of improvements ๐Ÿ˜†๐Ÿ˜†๐Ÿ˜†๐Ÿ˜†

import React from 'react';
import { atomFamily, RecoilRoot, useRecoilValue } from 'recoil';

// Creating atoms using atomFamily for state management
const atomFamilyState = atomFamily({
  key: 'atomFamilyState',
  default: (index) => `Value from Atom ${index + 1}`,
});

// Example consumer component that uses recoil state
const DeepConsumer = () => {
  return (
    <div>
      {Array.from({ length: 60 }).map((_, index) => {
        const value = useRecoilValue(atomFamilyState(index));
        return <p key={index}>{value}</p>;
      })}
    </div>
  );
};

// Root App Component
const App = () => {
  return (
    <RecoilRoot>
      <DeepConsumer />
    </RecoilRoot>
  );
};

export default App;

Conclusion

Migrating a nested React context tree to Recoil offers significant advantages in terms of code maintainability, performance, and simplicity. By eliminating the need for deeply nested context providers, Recoil streamlines state management, making it easier to debug and optimize. The use of atoms and a single provider enhances efficiency, while features like dynamic atom creation, useRecoilValue for read-only access, and Atom Families further refine the state management process. These improvements not only reduce unnecessary re-renders but also simplify the management of similar state pieces, ultimately leading to a more robust and scalable application architecture.

0
Subscribe to my newsletter

Read articles from Neelesh Roy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Neelesh Roy
Neelesh Roy