Recoil for Micro-FE State Management

Recoil for Micro-FE State Management

Micro-Frontends and Module Federation creates an interesting challenge for state management. Because you may not know the data requirements of the frontend. A naive implementation using useState or useContext could result in a system where the entire page is re-rendered on even the most minor state change, with most components just re-rendering what was already there.

Recoil has three features that help solve some of these issues.

  • State is modeled as a set of atoms, each of which has a single value.
  • You can create virtual atoms by using selectors that take data from several atoms (or other selectors) to make a new value that looks just like an atom to the consumer.
  • Updating any of the atoms (or selectors) updates just those specific components that depend on it.

These features mean that you have fine grained control over your state and how it's consumed.

The API

The recoil API is remarkably simple. You are required to have a RecoilRoot component at the top of the hierarchy.

import React from "react";
import { RecoilRoot } from "recoil";

const MyApp = () => (
  <RecoilRoot>
    ...My Component Tree...
  </RecoilRoot>
);

From there you need to create atoms that model your data. For example, you could create a name atom:

import { atom } from "recoil";

export const name = atom({
  key: "name",
  default: "Jack"
});

export const pets = atom({
  key: "pets",
  default: [ "Mimi", "Little Guy" ]
});

This creates two atoms. The first called name is a string value that is initialized with "Jack". The pets atom is initialized with an array with my two dogs names.

From there in a component you use them much like you would useState.

import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { name, pets } from "./atoms";

const MyView = () => {
  const [nameValue, nameSet] = useRecoilState(name);
  const petsValue = useRecoilValue(pets);
  
  return (
    <>
      <input
        value={nameValue}
        onChange={(evt) => nameSet(evt.target.value)}
      />
      <div>
        Pets: {petsValue.join(', ')}
      </div>
    </>
  );
}

Here the MyView component subscribes to the name atom in a read/write manner. But connects to the pets atom as read-only. It's nice to have that option. Otherwise the code reads almost identically to a useState example.

You can also create virtual atoms using selector, like so:

import { selector } from "recoil";
import { name, pets } from "./atoms";

export const person = selector({
  key: 'person',
  get: ({get}) => {
    return {
      name: get(name),
      pets: get(pets),
    };
  },
});

This selector would return an object with both name and pets, and any time either of those changed then the selector would force an update.

And to consume a selector is just as easy as an atom

import { useRecoilState, useRecoilValue } from "recoil";
import { person } from "./person-atoms";

const MyComponent = () => {
  const personValue = useRecoilValue(person);
};

Selectors can even have setters:

import { selector } from "recoil";
import { name, pets } from "./atoms";

export const person = selector({
  key: 'person',
  get: ({get}) => {
    return {
      name: get(name),
      pets: get(pets),
    };
  },
  set: ({set}, newValue) => {
    set(name, newValue.name);
    set(pets, newValue.pets);
  },
});

This selector could then be used with the set function returned from useRecoilState to set both name and pets atoms from a single object.

The Project

In the project video I take an existing e-Commerce style application that uses Federated Modules to share components between three applications, and extend that using Recoil to share state.