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)