Recoil

Facebook's experimental state management library with atoms and selectors

Recoil - React State Management by Meta

Recoil is an experimental state management library from Meta (Facebook). It was designed specifically for React and provides a more React-like approach to global state with atoms and selectors. While still experimental, it's used in production at Meta and offers powerful features for complex state management.

⚠️ Note

Recoil is still marked as experimental by Meta. For new projects, consider Jotai as an alternative with similar atomic concepts but more active development.

Core Concepts

  • Atoms — Units of state that components can subscribe to
  • Selectors — Derived state from atoms or other selectors
  • RecoilRoot — Provider component wrapping your app
  • Async Selectors — Handle async data with Suspense

Installation

npm install recoil

Setup and Basic Atoms

import { RecoilRoot, atom, useRecoilState, useRecoilValue } from 'recoil';

// Create atoms
const textState = atom({
  key: 'textState', // unique ID (required)
  default: '',      // default value
});

const countState = atom({
  key: 'countState',
  default: 0,
});

// Wrap your app with RecoilRoot
function App() {
  return (
    <RecoilRoot>
      <TextInput />
      <CharacterCount />
      <Counter />
    </RecoilRoot>
  );
}

// Use atoms in components
function TextInput() {
  const [text, setText] = useRecoilState(textState);
  
  return (
    <input
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
    />
  );
}

// Read-only access
function Counter() {
  const count = useRecoilValue(countState);
  return <span>Count: {count}</span>;
}

Selectors - Derived State

import { selector, useRecoilValue } from 'recoil';

// Selector derived from atom
const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

// Multiple dependencies
const todoStatsState = selector({
  key: 'todoStatsState',
  get: ({ get }) => {
    const todos = get(todoListState);
    const totalNum = todos.length;
    const completedNum = todos.filter(t => t.completed).length;
    const uncompletedNum = totalNum - completedNum;
    const percentComplete = totalNum === 0 ? 0 : Math.round((completedNum / totalNum) * 100);
    
    return {
      totalNum,
      completedNum,
      uncompletedNum,
      percentComplete,
    };
  },
});

function TodoStats() {
  const { totalNum, completedNum, percentComplete } = useRecoilValue(todoStatsState);
  
  return (
    <div>
      <p>Total: {totalNum}</p>
      <p>Completed: {completedNum}</p>
      <p>Progress: {percentComplete}%</p>
    </div>
  );
}

Writeable Selectors

import { selector, useRecoilState } from 'recoil';

const celsiusState = atom({
  key: 'celsiusState',
  default: 25,
});

const fahrenheitState = selector({
  key: 'fahrenheitState',
  get: ({ get }) => get(celsiusState) * 9/5 + 32,
  set: ({ set }, newValue) => {
    const celsius = (newValue - 32) * 5/9;
    set(celsiusState, celsius);
  },
});

function TemperatureConverter() {
  const [celsius, setCelsius] = useRecoilState(celsiusState);
  const [fahrenheit, setFahrenheit] = useRecoilState(fahrenheitState);
  
  return (
    <div>
      <input
        type="number"
        value={celsius}
        onChange={(e) => setCelsius(Number(e.target.value))}
      />°C
      <input
        type="number"
        value={fahrenheit}
        onChange={(e) => setFahrenheit(Number(e.target.value))}
      />°F
    </div>
  );
}

Async Selectors

import { selector, useRecoilValue } from 'recoil';
import { Suspense } from 'react';

const userIdState = atom({
  key: 'userIdState',
  default: 1,
});

// Async selector - automatically integrates with Suspense
const currentUserState = selector({
  key: 'currentUserState',
  get: async ({ get }) => {
    const userId = get(userIdState);
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

const userPostsState = selector({
  key: 'userPostsState',
  get: async ({ get }) => {
    const user = await get(currentUserState);
    const response = await fetch(`/api/users/${user.id}/posts`);
    return response.json();
  },
});

function UserProfile() {
  const user = useRecoilValue(currentUserState);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <RecoilRoot>
      <Suspense fallback={<div>Loading...</div>}>
        <UserProfile />
      </Suspense>
    </RecoilRoot>
  );
}

Atom Family - Dynamic Atoms

import { atomFamily, selectorFamily, useRecoilState } from 'recoil';

// Create atoms dynamically with parameters
const todoItemState = atomFamily({
  key: 'todoItemState',
  default: (id) => ({
    id,
    text: '',
    completed: false,
  }),
});

// Selector family for parameterized selectors
const userByIdState = selectorFamily({
  key: 'userByIdState',
  get: (userId) => async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

function TodoItem({ id }) {
  const [item, setItem] = useRecoilState(todoItemState(id));
  
  return (
    <div>
      <input
        type="checkbox"
        checked={item.completed}
        onChange={() => setItem({ ...item, completed: !item.completed })}
      />
      <span>{item.text}</span>
    </div>
  );
}

function UserCard({ userId }) {
  const user = useRecoilValue(userByIdState(userId));
  return <div>{user.name}</div>;
}

Persistence with Effects

import { atom, AtomEffect } from 'recoil';

// Effect for localStorage persistence
const localStorageEffect = (key) => ({ setSelf, onSet }) => {
  const savedValue = localStorage.getItem(key);
  if (savedValue != null) {
    setSelf(JSON.parse(savedValue));
  }
  
  onSet((newValue, _, isReset) => {
    isReset
      ? localStorage.removeItem(key)
      : localStorage.setItem(key, JSON.stringify(newValue));
  });
};

// Use effect in atom
const userPrefsState = atom({
  key: 'userPrefsState',
  default: {
    theme: 'light',
    language: 'en',
  },
  effects: [
    localStorageEffect('user_preferences'),
  ],
});

Recoil vs Jotai

Feature Recoil Jotai
Provider Required (RecoilRoot) Optional
Atom keys Required (strings) Not needed
Bundle size ~20KB ~3KB
Status Experimental Stable
Maintainer Meta Poimandres (Pmndrs)

💡 Key Takeaways

  • • Recoil uses atoms and selectors similar to Jotai
  • • Requires unique string keys for all atoms/selectors
  • • Great Suspense integration for async data
  • • Atom/selector families for dynamic state
  • • Consider Jotai for new projects (smaller, more active)