TechLead

Recoil

Biblioteca experimental de gestión de estado de Facebook con átomos y selectores

Recoil - Gestión de Estado de React por Meta

Recoil es una biblioteca experimental de gestión de estado de Meta (Facebook). Fue diseñada específicamente para React y proporciona un enfoque más parecido a React para el estado global con átomos y selectores. Aunque todavía es experimental, se usa en producción en Meta y ofrece características poderosas para gestión de estado compleja.

⚠️ Nota

Recoil todavía está marcado como experimental por Meta. Para proyectos nuevos, considera Jotai como una alternativa con conceptos atómicos similares pero desarrollo más activo.

Conceptos Clave

  • Átomos — Unidades de estado a las que los componentes pueden suscribirse
  • Selectores — Estado derivado de átomos u otros selectores
  • RecoilRoot — Componente provider que envuelve tu app
  • Selectores Asíncronos — Maneja datos asíncronos con Suspense

Instalación

npm install recoil

Configuración y Átomos Básicos

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

// Crea átomos
const textState = atom({
  key: 'textState', // ID único (requerido)
  default: '',      // valor por defecto
});

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

// Envuelve tu app con RecoilRoot
function App() {
  return (
    <RecoilRoot>
      <TextInput />
      <CharacterCount />
      <Counter />
    </RecoilRoot>
  );
}

// Usa átomos en componentes
function TextInput() {
  const [text, setText] = useRecoilState(textState);
  
  return (
    <input
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
    />
  );
}

// Acceso de solo lectura
function Counter() {
  const count = useRecoilValue(countState);
  return <span>Cuenta: {count}</span>;
}

Selectores - Estado Derivado

import { selector, useRecoilValue } from 'recoil';

// Selector derivado de átomo
const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

// Múltiples dependencias
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>Completadas: {completedNum}</p>
      <p>Progreso: {percentComplete}%</p>
    </div>
  );
}

Selectores Escribibles

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>
  );
}

Selectores Asíncronos

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

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

// Selector asíncrono - se integra automáticamente con 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>Cargando...</div>}>
        <UserProfile />
      </Suspense>
    </RecoilRoot>
  );
}

Familia de Átomos - Átomos Dinámicos

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

// Crea átomos dinámicamente con parámetros
const todoItemState = atomFamily({
  key: 'todoItemState',
  default: (id) => ({
    id,
    text: '',
    completed: false,
  }),
});

// Familia de selectores para selectores parametrizados
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>;
}

Persistencia con Efectos

import { atom, AtomEffect } from 'recoil';

// Efecto para persistencia en localStorage
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));
  });
};

// Usa efecto en átomo
const userPrefsState = atom({
  key: 'userPrefsState',
  default: {
    theme: 'light',
    language: 'es',
  },
  effects: [
    localStorageEffect('user_preferences'),
  ],
});

Recoil vs Jotai

Característica Recoil Jotai
Provider Requerido (RecoilRoot) Opcional
Claves de átomos Requeridas (strings) No necesarias
Tamaño del bundle ~20KB ~3KB
Estado Experimental Estable
Mantenedor Meta Poimandres (Pmndrs)

💡 Puntos Clave

  • • Recoil usa átomos y selectores similares a Jotai
  • • Requiere claves string únicas para todos los átomos/selectores
  • • Gran integración con Suspense para datos asíncronos
  • • Familias de átomos/selectores para estado dinámico
  • • Considera Jotai para proyectos nuevos (más pequeño, más activo)