agent-rules-kit
Version:
CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.
654 lines (544 loc) • 16.5 kB
Markdown
# Recoil State Management in React
Recoil is a state management library for React applications that provides a flexible and efficient way to share state across components. Created by Facebook, it uses a graph-based approach with atoms and selectors.
## Core Concepts
1. **Atoms**: Basic units of state that components can subscribe to
2. **Selectors**: Pure functions that derive state from atoms or other selectors
3. **Atom Families & Selector Families**: Collections of atoms or selectors created from a template
4. **Hooks**: React hooks to read and write state (useRecoilState, useRecoilValue, etc.)
5. **Concurrency Model**: Works with React's concurrent mode
## Directory Structure
```
src/
├── recoil/
│ ├── index.js # Exports all atoms and selectors
│ ├── atoms/
│ │ ├── authAtoms.js # Authentication-related atoms
│ │ ├── uiAtoms.js # UI-related atoms
│ │ └── todoAtoms.js # Todo-related atoms
│ ├── selectors/
│ │ ├── authSelectors.js # Authentication-related selectors
│ │ ├── uiSelectors.js # UI-related selectors
│ │ └── todoSelectors.js # Todo-related selectors
│ └── effects/
│ ├── persistence.js # Effects for persistence
│ └── validation.js # Effects for validation
└── components/
├── TodoList.jsx # Component using todo atoms/selectors
├── UserProfile.jsx # Component using auth atoms/selectors
└── ThemeToggle.jsx # Component using ui atoms/selectors
```
## Setup and Installation
First, install Recoil:
```bash
npm install recoil
# or with yarn
yarn add recoil
```
Then, wrap your app with `RecoilRoot`:
```jsx
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>
);
```
## Implementation
### Basic Atoms and Selectors
```javascript
// recoil/atoms/todoAtoms.js
import { atom, atomFamily } from 'recoil';
// An atom representing the list of todo IDs
export const todoListState = atom({
key: 'todoListState',
default: [],
});
// An atom family representing individual todo items
export const todoItemState = atomFamily({
key: 'todoItemState',
default: (id) => ({
id,
text: '',
completed: false,
createdAt: new Date().toISOString(),
}),
});
// An atom for the filter criteria
export const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'all', // 'all' | 'completed' | 'uncompleted'
});
```
```javascript
// recoil/selectors/todoSelectors.js
import { selector, selectorFamily } from 'recoil';
import {
todoListState,
todoItemState,
todoListFilterState,
} from '../atoms/todoAtoms';
// A selector that returns the filtered todo list
export const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({ get }) => {
const filter = get(todoListFilterState);
const todoList = get(todoListState);
return todoList
.map((id) => get(todoItemState(id)))
.filter((item) => {
switch (filter) {
case 'completed':
return item.completed;
case 'uncompleted':
return !item.completed;
default:
return true;
}
});
},
});
// A selector that returns todo statistics
export const todoStatsState = selector({
key: 'todoStatsState',
get: ({ get }) => {
const todoList = get(todoListState).map((id) => get(todoItemState(id)));
const totalNum = todoList.length;
const completedNum = todoList.filter((item) => item.completed).length;
const uncompletedNum = totalNum - completedNum;
const percentCompleted =
totalNum === 0 ? 0 : Math.round((completedNum / totalNum) * 100);
return {
totalNum,
completedNum,
uncompletedNum,
percentCompleted,
};
},
});
// A selector family that returns a specific todo by ID with additional info
export const enhancedTodoItemState = selectorFamily({
key: 'enhancedTodoItemState',
get:
(id) =>
({ get }) => {
const item = get(todoItemState(id));
// Add additional derived information
return {
...item,
isOverdue: item.dueDate
? new Date(item.dueDate) < new Date()
: false,
};
},
});
```
### Using Atoms with Effects for Persistence
```javascript
// recoil/effects/persistence.js
import { todoListState, todoItemState } from '../atoms/todoAtoms';
// Effect to persist todos in localStorage
export const localStorageEffect =
(key) =>
({ setSelf, onSet, trigger }) => {
// If there's saved data, load it on initialization
if (trigger === 'get') {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
}
// Save to localStorage on change
onSet((newValue, _, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
// Then use it in atom definition:
export const persistedTodoListState = atom({
key: 'persistedTodoListState',
default: [],
effects: [localStorageEffect('todo_list')],
});
```
### Async Selectors
```javascript
// recoil/selectors/userSelectors.js
import { selector, selectorFamily } from 'recoil';
import { userState } from '../atoms/userAtoms';
// Async selector that fetches user data
export const userDataQuery = selectorFamily({
key: 'userDataQuery',
get: (userId) => async () => {
if (!userId) return null;
try {
const response = await fetch(
`https://api.example.com/users/${userId}`
);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
},
});
// Selector that combines local state with async data
export const combinedUserProfileState = selectorFamily({
key: 'combinedUserProfileState',
get:
(userId) =>
({ get }) => {
const userData = get(userDataQuery(userId));
const localUserState = get(userState);
return {
...userData,
...localUserState,
lastUpdated: new Date().toISOString(),
};
},
});
```
### Component Implementation Examples
```jsx
// components/TodoList.jsx
import React from 'react';
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
import {
todoListState,
todoItemState,
todoListFilterState,
} from '../recoil/atoms/todoAtoms';
import {
filteredTodoListState,
todoStatsState,
} from '../recoil/selectors/todoSelectors';
const TodoList = () => {
const todoList = useRecoilValue(filteredTodoListState);
const todoStats = useRecoilValue(todoStatsState);
const [filter, setFilter] = useRecoilState(todoListFilterState);
const setTodoList = useSetRecoilState(todoListState);
const addTodo = () => {
const id = Date.now().toString();
setTodoList((oldList) => [...oldList, id]);
};
return (
<div className='todo-container'>
<div className='stats'>
<p>Total: {todoStats.totalNum}</p>
<p>Completed: {todoStats.completedNum}</p>
<p>Progress: {todoStats.percentCompleted}%</p>
</div>
<div className='filters'>
<button
onClick={() => setFilter('all')}
className={filter === 'all' ? 'active' : ''}>
All
</button>
<button
onClick={() => setFilter('completed')}
className={filter === 'completed' ? 'active' : ''}>
Completed
</button>
<button
onClick={() => setFilter('uncompleted')}
className={filter === 'uncompleted' ? 'active' : ''}>
Uncompleted
</button>
</div>
<ul className='todo-list'>
{todoList.map((todo) => (
<TodoItem key={todo.id} id={todo.id} />
))}
</ul>
<button onClick={addTodo}>Add Todo</button>
</div>
);
};
const TodoItem = ({ id }) => {
const [todo, setTodo] = useRecoilState(todoItemState(id));
const setTodoList = useSetRecoilState(todoListState);
const updateText = (event) => {
setTodo({
...todo,
text: event.target.value,
});
};
const toggleCompleted = () => {
setTodo({
...todo,
completed: !todo.completed,
});
};
const deleteTodo = () => {
setTodoList((oldList) => oldList.filter((itemId) => itemId !== id));
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type='checkbox'
checked={todo.completed}
onChange={toggleCompleted}
/>
<input
type='text'
value={todo.text}
onChange={updateText}
placeholder='Enter a task...'
/>
<button onClick={deleteTodo}>Delete</button>
</li>
);
};
export default TodoList;
```
```jsx
// components/UserProfile.jsx
import React, { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { combinedUserProfileState } from '../recoil/selectors/userSelectors';
const UserProfile = ({ userId }) => {
return (
<div className='user-profile'>
<h2>User Profile</h2>
<Suspense fallback={<div>Loading user data...</div>}>
<UserData userId={userId} />
</Suspense>
</div>
);
};
// This component will suspend while loading async data
const UserData = ({ userId }) => {
// This will automatically handle loading states (suspense)
const userData = useRecoilValue(combinedUserProfileState(userId));
if (!userData) {
return <div>No user data available</div>;
}
return (
<div className='user-data'>
<h3>{userData.name}</h3>
<p>Email: {userData.email}</p>
<p>Location: {userData.location}</p>
<p>
Last Updated: {new Date(userData.lastUpdated).toLocaleString()}
</p>
</div>
);
};
export default UserProfile;
```
## Advanced Features and Patterns
### Atom Effects
Atom effects allow you to add side effects to atoms, such as synchronization with external storage, logging, or validation.
```javascript
// recoil/effects/validation.js
export const validationEffect =
(validator) =>
({ setSelf, onSet }) => {
onSet((newValue, oldValue) => {
if (!validator(newValue)) {
// You can either:
// 1. Throw an error
throw new Error('Invalid state!');
// 2. Or revert to the old value
// setSelf(oldValue);
}
});
};
// Then use the effect in an atom
export const emailState = atom({
key: 'emailState',
default: '',
effects: [
validationEffect((email) => {
// Simple validation rule
return /\S+@\S+\.\S+/.test(email) || email === '';
}),
],
});
```
### Transaction Observer
You can create a global observer for state changes:
```javascript
// recoil/observers.js
import { useTransactionObservation_UNSTABLE } from 'recoil';
export function StateChangeLogger() {
useTransactionObservation_UNSTABLE(
({ atomValues, atomInfo, modifiedAtoms }) => {
for (const modifiedAtom of modifiedAtoms) {
console.log(`Atom ${modifiedAtom} was modified`);
console.log('New value:', atomValues.get(modifiedAtom));
}
}
);
return null;
}
// Then add it to your app
function App() {
return (
<RecoilRoot>
<StateChangeLogger />
{/* Rest of your app */}
</RecoilRoot>
);
}
```
### Snapshot Hooks for Time-Travel Debugging
```javascript
// components/TimeTravel.jsx
import React, { useState, useCallback } from 'react';
import {
useGotoRecoilSnapshot,
useRecoilSnapshot,
useRecoilCallback,
} from 'recoil';
const TimeTravel = () => {
const [snapshots, setSnapshots] = useState([]);
const gotoSnapshot = useGotoRecoilSnapshot();
// Get the current snapshot
const currentSnapshot = useRecoilSnapshot();
// Add current snapshot to history
const takeSnapshot = useCallback(() => {
setSnapshots((snaps) => [...snaps, currentSnapshot]);
}, [currentSnapshot]);
// Go to a specific snapshot
const goToSnapshot = useCallback(
(index) => {
const snapshot = snapshots[index];
gotoSnapshot(snapshot);
},
[gotoSnapshot, snapshots]
);
return (
<div className='time-travel-debugger'>
<h3>Time Travel Debugging</h3>
<button onClick={takeSnapshot}>Take Snapshot</button>
<div className='snapshots'>
<h4>Snapshots ({snapshots.length})</h4>
<ul>
{snapshots.map((_, index) => (
<li key={index}>
<button onClick={() => goToSnapshot(index)}>
Go to Snapshot {index + 1}
</button>
</li>
))}
</ul>
</div>
</div>
);
};
export default TimeTravel;
```
## Best Practices
### 1. Atom Organization
- Group related atoms in files based on domain/feature
- Use consistent naming patterns (e.g., `entityNameState`)
- Consider splitting large domains into multiple files
- Export atoms via a barrel file (index.js)
### 2. Selector Patterns
- Use selectors to derive state rather than duplicating logic
- Create smaller, composable selectors
- Use memoization for expensive calculations
- Consider performance implications of complex selectors
### 3. Error Handling
- Handle promise rejections in async selectors
- Set up error boundaries around suspense components
- Use `errorSelector` to transform errors
```jsx
const errorSelector = selector({
key: 'errorSelector',
get: async ({ get }) => {
try {
return get(myDataQuery);
} catch (error) {
// transform or log error
throw new Error(`Friendly error message: ${error.message}`);
}
},
});
```
### 4. Data Fetching Patterns
- Use suspense for async data loading
- Consider implementing request deduplication
- Cache responses appropriately
- Handle loading and error states gracefully
### 5. Performance Considerations
- Use atomFamily and selectorFamily for collections
- Split large components into smaller ones that subscribe to specific atoms
- Use `useRecoilCallback` for complex state updates
- Profile using the React DevTools and Recoil Debug Observer
### 6. TypeScript Integration
```typescript
// recoil/atoms/todoAtoms.ts
import { atom, atomFamily, AtomEffect } from 'recoil';
// Define types
export interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: string;
dueDate?: string;
}
export type TodoFilter = 'all' | 'completed' | 'uncompleted';
// Type the atoms
export const todoListState = atom<string[]>({
key: 'todoListState',
default: [],
});
export const todoItemState = atomFamily<Todo, string>({
key: 'todoItemState',
default: (id) => ({
id,
text: '',
completed: false,
createdAt: new Date().toISOString(),
}),
});
export const todoListFilterState = atom<TodoFilter>({
key: 'todoListFilterState',
default: 'all',
});
```
## When to Choose Recoil
Recoil is particularly well-suited for:
- React-only applications (it's tightly integrated with React)
- Applications that need to share state between distant components
- Projects that need fine-grained reactivity
- Applications that can benefit from asynchronous selectors
- Teams that prefer a simpler API than Redux
- Apps that need to work well with React Concurrent Mode and Suspense
Recoil may not be the best choice when:
- You need a non-React state solution
- You require a more mature ecosystem (Recoil is newer than Redux)
- You need time-tested patterns and extensive documentation
- Your application is very small and simple
## Comparison with Other State Management Libraries
### Recoil vs. Redux
- **API Complexity**: Recoil has a simpler API with fewer concepts to learn
- **Boilerplate**: Recoil requires less boilerplate code
- **React Integration**: Recoil is more tightly integrated with React
- **Async Handling**: Recoil has built-in support for async operations via selectors
- **Maturity**: Redux has a more mature ecosystem and community
### Recoil vs. MobX
- **Paradigm**: Recoil uses atoms/selectors, MobX uses observable objects
- **Reactivity Model**: Recoil is more explicit, MobX can be more "magical"
- **Learning Curve**: Recoil may be easier to learn for React developers
- **Performance**: Both offer good performance with different optimization strategies
- **Debugging**: Recoil has built-in time-travel debugging capabilities
### Recoil vs. Context API
- **Performance**: Recoil has better performance for frequent updates
- **Granularity**: Recoil allows for more granular subscriptions
- **Features**: Recoil offers more features (async, derived state, etc.)
- **Complexity**: Context API is simpler but less powerful
- **Bundle Size**: Context API doesn't add any bundle size