# API Integration Standards for Recoil
This document outlines the coding standards for integrating APIs with Recoil, focusing on maintainability, performance, and security. It's designed to guide developers and inform AI coding assistants to produce high-quality Recoil code.
## 1. Architecture and Patterns
### 1.1. Separation of Concerns
**Standard:** Decouple API interaction logic from Recoil state management and UI components.
**Do This:** Implement a dedicated layer (e.g., services or repositories) responsible for API calls. This layer should transform API responses into a format suitable for Recoil atoms/selectors.
**Don't Do This:** Make direct API calls within React components or Recoil atoms/selectors.
**Why:** Separation of concerns improves code testability, reusability, and reduces the cognitive load. Changes to the API or data fetching logic won't necessitate modifications in React components or Recoil state.
**Example:**
"""typescript
// api/productService.ts
import axios from 'axios';
const API_URL = 'https://api.example.com/products';
export const fetchProducts = async () => {
try {
const response = await axios.get(API_URL);
return response.data; // Assuming response.data is an array of product objects
} catch (error) {
console.error('Error fetching products:', error);
throw error; // Re-throw the error to be handled by the caller.
}
};
export const createProduct = async (productData: any) => {
try {
const response = await axios.post(API_URL, productData);
return response.data;
} catch (error) {
console.error('Error creating product:', error);
throw error;
}
};
"""
"""typescript
// recoil/productState.ts
import { atom, selector } from 'recoil';
import { fetchProducts } from '../api/productService';
export const productListState = atom({
key: 'productListState',
default: [],
});
export const productListSelector = selector({
key: 'productListSelector',
get: async ({ get }) => {
try {
const products = await fetchProducts();
return products;
} catch (error) {
// Handle error appropriately, e.g., show an error message
console.error("Failed to fetch products in selector:", error);
return get(productListState); //Return previous state or default state.
}
},
});
"""
### 1.2. Asynchronous Selectors
**Standard:** Use asynchronous selectors for API-driven data fetching in Recoil.
**Do This:** Leverage "selector" with an "async" "get" function to handle API calls within a Recoil selector.
**Don't Do This:** Rely solely on "useEffect" in components to fetch data and update atoms, particularly if other Recoil state depends on that data.
**Why:** Asynchronous selectors automatically manage the loading state and memoize the result, preventing unnecessary re-fetches. They also handle dependencies between different pieces of state better than "useEffect".
**Example:**
"""typescript
// recoil/userState.ts
import { atom, selector } from 'recoil';
const USER_API_URL = 'https://api.example.com/user';
interface User {
id: number;
name: string;
email: string;
}
export const userIdState = atom({
key: 'userIdState',
default: 1, // Example initial user ID
});
export const userState = atom({
key: 'userState',
default: null,
})
export const userSelector = selector({
key: 'userSelector',
get: async ({ get }) => {
const userId = get(userIdState);
const response = await fetch("${USER_API_URL}/${userId}");
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
return await response.json();
},
});
"""
### 1.3. Error Handling
**Standard:** Implement robust error handling within your API integration logic.
**Do This:** Wrap API calls in "try...catch" blocks. Manage loading and error states within your Recoil selectors or components. Utilize dedicated error boundary components to catch and display errors gracefully.
**Don't Do This:** Ignore errors or let them bubble up unhandled. Assume API calls will always succeed.
**Why:** Proper error handling prevents silent failures and provides a better user experience. Clear error messages assist in efficiently debugging and resolving issues.
**Example:**
"""typescript
// recoil/dataState.ts
import { atom, selector } from 'recoil';
interface Data {
id: number;
value: string;
}
export const dataIsLoadingState = atom({
key: 'dataIsLoadingState',
default: false,
});
export const dataErrorState = atom({
key: 'dataErrorState',
default: null as Error | null,
});
export const dataState = selector({
key: 'dataState',
get: async ({ set }) => {
set(dataIsLoadingState, true);
set(dataErrorState, null); // Clear any previous errors
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const data = await response.json();
set(dataIsLoadingState, false);
return data;
} catch (error: any) {
set(dataIsLoadingState, false);
set(dataErrorState, error);
throw error; // Re-throw the error to be caught by an error boundary if needed
}
},
});
"""
"""typescript
// ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class ErrorBoundary extends Component {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error) {
// Update state so the next render will show the fallback UI.
return { hasError: true, error: error, errorInfo: null };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// You can also log the error to an error reporting service
console.error("Caught error:", error, errorInfo);
this.setState({error: error, errorInfo: errorInfo})
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
Something went wrong.
<p>{this.state.error?.message}</p>
{this.state.errorInfo?.componentStack}
);
}
return this.props.children;
}
}
export default ErrorBoundary;
"""
"""typescript
// Usage in App.tsx
import ErrorBoundary from './ErrorBoundary';
import SomeComponentThatFetchesData f
function App() {
return (
);
}
"""
### 1.4. Optimistic Updates
**Standard:** When appropriate, implement optimistic updates for improved perceived performance during create/update operations.
**Do This:** Immediately update the Recoil state based on the user's input, *before* the API call completes. Revert the state if the API call fails.
**Don't Do This:** Wait for the API call to complete before updating the UI.
**Why:** Provides a snappier user experience. Optimistic updates eliminate perceived latency for common operations. Handle remote validation for unexpected results.
**Example:**
"""typescript
// recoil/todoListState.ts
import { atom, selector, useRecoilState } from 'recoil';
interface Todo {
id: string;
text: string;
isComplete: boolean;
}
export const todoListState = atom({
key: 'todoListState',
default: [],
});
const API_URL = 'https://api.example.com/todos';
// Async selector to fetch todos from API
export const todoListSelector = selector({
key: 'todoListSelector',
get: async ({get}) => {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
return await response.json();
},
});
export const useAddTodo = () => {
const [todos, setTodos] = useRecoilState(todoListState);
const addTodoOptimistic = async (text: string) => {
const newTodo: Todo = {
id: Math.random().toString(36).substring(7), // Temporary ID
text,
isComplete: false,
};
// Optimistic update
setTodos([...todos, newTodo]);
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
});
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const serverTodo: Todo = await response.json();
// Replace temporary ID with server ID
setTodos(todos.map(todo => todo.id === newTodo.id ? serverTodo : todo));
} catch (error) {
console.error('Error creating todo:', error);
// Revert optimistic update on error
setTodos(todos.filter(todo => todo.id !== newTodo.id));
}
};
return addTodoOptimistic;
};
"""
## 2. Data Handling
### 2.1. Data Transformation
**Standard:** Transform API responses into a consistent and type-safe format before storing them in Recoil atoms.
**Do This:** Create dedicated functions or classes to map API responses to your application's data models.
**Don't Do This:** Store raw API responses directly in Recoil atoms.
**Why:** Improves code maintainability and reduces coupling with the specific structure of the API response. Type safety ensures consistent data throughout the application. Prevents breaking changes down the line.
**Example:**
"""typescript
// api/mappers.ts
interface ApiResponse {
id: number;
product_name: string;
price_usd: number;
}
interface Product {
id: number;
name: string;
price: number; // Stored in cents to avoid floating point issues
}
export const mapApiResponseToProduct = (apiResponse: ApiResponse): Product => {
return {
id: apiResponse.id,
name: apiResponse.product_name,
price: Math.round(apiResponse.price_usd * 100) // Convert to cents
};
};
export const mapProductToApiResponse = (product: Product): ApiResponse => {
return {
id: product.id,
product_name: product.name,
price_usd: product.price / 100, // Convert to USD
};
};
"""
"""typescript
// recoil/productState.ts
import { atom, selector } from 'recoil';
import { fetchProducts } from '../api/productService';
import { mapApiResponseToProduct } from '../api/mappers';
export const productListState = atom({
key: 'productListState',
default: [],
});
//productList selector
export const productListSelector = selector({
key: 'productListSelector',
get: async () => {
const apiResponse = await fetchProducts(); // Assuming fetchProducts returns an array of ApiResponse
return apiResponse.map(mapApiResponseToProduct);
},
});
"""
### 2.2. Data Validation
**Standard:** Validate data received from APIs to ensure data integrity and prevent unexpected errors.
**Do This:** Use libraries like Zod, Yup, or io-ts to define schemas and validate API responses before storing them in Recoil atoms.
**Don't Do This:** Trust that API responses will always conform to the expected format.
**Why:** Protects against malformed data, preventing crashes or unexpected behavior. Provides early detection of API changes or inconsistencies.
**Example (using Zod):**
"""typescript
// api/schemas.ts
import { z } from 'zod';
export const ProductSchema = z.object({
id: z.number().positive(),
product_name: z.string().min(1),
price_usd: z.number().nonnegative(),
});
export const ProductArraySchema = z.array(ProductSchema);
"""
"""typescript
// api/productService.ts
import axios from 'axios';
import { ProductArraySchema } from './schemas';
const API_URL = 'https://api.example.com/products';
export const fetchProducts = async () => {
try {
const response = await axios.get(API_URL);
const parsedData = ProductArraySchema.parse(response.data);
return parsedData;
} catch (error) {
console.error('Error fetching products:', error);
throw error;
}
};
"""
### 2.3. Data Normalization
**Standard:** Normalize data received from APIs to reduce redundancy and improve data consistency.
**Do This:** Structure your Recoil atoms as normalized stores, where entities are stored in separate collections keyed by their IDs. Use selectors to join and derive data as needed by the UI.
**Don't Do This:** Store denormalized or deeply nested data directly in Recoil atoms.
**Why:** Normalization makes updates more efficient and prevents data inconsistencies. Reduces the risk of accidentally modifying data in multiple places, thus improving performance.
**Example:**
"""typescript
// recoil/entities.ts
import { atom } from 'recoil';
interface User {
id: string;
name: string;
email: string;
}
export const usersState = atom>({
key: 'usersState',
default: {}, // Store users keyed by their IDs
});
interface Post {
id: string;
userId: string;
title: string;
body: string;
}
export const postsState = atom>({
key: 'postsState',
default: {}, // Store posts keyed by their IDs
});
"""
"""typescript
// recoil/selectors.ts
import { selector } from 'recoil';
import { usersState, postsState } from './entities';
export const userPostsSelector = selector({
key: 'userPostsSelector',
get: ({ get }) => {
const users = get(usersState);
const posts = get(postsState);
// Implement logic to join users and posts based on userId
return Object.values(posts).map((post) => ({
...post,
user: users[post.userId]
}));
},
});
"""
## 3. Network Requests
### 3.1. Request Deduplication
**Standard:** Implement request deduplication to prevent sending redundant API requests.
**Do This:** If the same selector is being read multiple times simultaneously, ensure that fetching is only triggered once. Recoil's memoization helps, but consider a dedicated deduplication layer for complex scenarios.
**Don't Do This:** Allow multiple identical API requests to be triggered concurrently.
**Why:** Reduces server load and improves application performance. Prevents race conditions and data inconsistencies.
**Example:**
Recoil's selectors provide built-in memoization. If multiple components read the same selector concurrently, the API request will only be dispatched once.
However, for more complex scenarios, consider using a library like "axios-hooks", which has built-in request deduplication capabilities.
"""typescript
import { useAxios } from 'axios-hooks'
import { productListState } from './recoil/productState'
import { useSetRecoilState } from 'recoil'
const ProductList = () => {
const setProducts = useSetRecoilState(productListState);
const [{ data, loading, error }, refetch] = useAxios(
'https://api.example.com/products'
)
useEffect(() => {
if (data) {
setProducts(data)
}
}, [data, setProducts])
}
"""
### 3.2. Caching
**Standard:** Implement caching strategies to reduce network requests and improve performance.
**Do This:** Cache API responses using browser storage (e.g., localStorage, sessionStorage, IndexedDB) or a server-side caching layer (e.g., Redis, Memcached). Consider using the Recoil "atomFamily" to create individual atoms/selectors for each resource, allowing for more granular caching. Use "Recoilize" to persist recoil state easily.
**Don't Do This:** Rely solely on browser caching without implementing proper cache invalidation strategies.
**Why:** Significantly improves application load times and reduces server load. Provides a better user experience, especially for users with slow or unreliable network connections.
**Example:**
"""typescript
// recoil/cachedDataState.ts
import { atom, selector } from 'recoil';
import { localStorageEffect } from 'recoiljs-toolkit';
const API_URL = 'https://api.example.com/data';
interface Data {
id: number;
value: string;
}
//Persist recoil state to local storage
export const cachedDataState = atom({
key: 'cachedDataState',
default: null,
effects: [
localStorageEffect('cachedData'),
],
});
export const fetchDataSelector = selector({
key: 'fetchDataSelector',
get: async ({get}) => {
const cachedData = get(cachedDataState);
if(cachedData){
console.log("Data retrieved from cache");
return cachedData;
}
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const data = await response.json();
return data;
},
});
"""
### 3.3. Throttling and Debouncing
**Standard:** Implement throttling or debouncing to limit the frequency of API calls triggered by user input.
**Do This:** Use libraries like Lodash's "throttle" or "debounce" functions to control the rate at which API requests are sent.
**Don't Do This:** Send API requests on every keystroke or mouse movement, especially for autosave functionality.
**Why:** Reduces server load and improves application responsiveness. Prevents overwhelming the API with excessive requests.
**Example:**
"""typescript
import { atom, selector } from 'recoil';
import { debounce } from 'lodash';
export const searchTermState = atom({
key: 'searchTermState',
default: '',
});
export const debouncedSearchTermState = atom({
key: 'debouncedSearchTermState',;
default: '',
});
export const debouncedSearchEffect = ({setSelf, onSet}:any) => {
onSet((newValue:string) => {
const debounced = debounce((newValue:string) => {
setSelf(newValue)
}, 500);
debounced(newValue)
})
}
export const searchTermSelector = selector({
key: 'searchTermSelector',
get: ({ get }) => {
const term = get(debouncedSearchTermState);
// Call the API with the debounced search term
},
});
"""
## 4. Security
### 4.1. Authentication and Authorization
**Standard:** Implement proper authentication and authorization mechanisms to protect your API endpoints.
**Do This:** Use secure authentication protocols like OAuth 2.0 or JWT (JSON Web Tokens). Store API keys and credentials securely (e.g., using environment variables or a secrets management system). Implement authorization checks on the server-side to ensure users only have access to the resources they are allowed to access.
**Don't Do This:** Store API keys directly in client-side code or expose them in network requests. Rely solely on client-side authorization checks.
**Why:** Protects sensitive data and prevents unauthorized access to your API. Ensures data integrity and compliance with security regulations.
### 4.2. Data Sanitization
**Standard:** Sanitize any user-provided data before sending it to the API to prevent injection attacks.
**Do This:** Use libraries like DOMPurify to sanitize HTML input. Escape user-provided data before including it in database queries or API requests.
**Don't Do This:** Trust that user input is safe and does not contain malicious code.
**Why:** Prevents security vulnerabilities like cross-site scripting (XSS) and SQL injection. Ensures that your application is protected against malicious attacks.
### 4.3. Rate Limiting
**Standard:** Implement rate limiting on your API endpoints to prevent abuse and denial-of-service (DoS) attacks.
**Do This:** Use middleware or server-side configuration to limit the number of requests that can be made from a single IP address or user account within a specific time period.
**Don't Do This:** Allow unlimited requests to your API endpoints, which could be exploited by attackers.
**Why:** Protects your API from abuse and ensures availability for legitimate users. Prevents attackers from overwhelming your server with malicious traffic.
## 5. Testing
### 5.1. Unit Tests
**Standard:** Write unit tests for your API integration logic, including API service functions and data mappers.
**Do This:** Mock API responses and verify that your code correctly transforms and processes the data. Test error handling scenarios to ensure that errors are handled gracefully.
**Don't Do This:** Skip unit tests for API integration logic, which could lead to undetected errors and regressions.
**Example:**
"""typescript
// api/productService.test.ts
import { fetchProducts } from './productService';
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked;
describe('productService', () => {
it('should fetch products successfully', async () => {
const mockProducts = [{ id: 1, name: 'Product 1', price: 10 }];
mockedAxios.get.mockResolvedValue({ data: mockProducts });
const products = await fetchProducts();
expect(products).toEqual(mockProducts);
expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/products');
});
it('should handle errors gracefully', async () => {
mockedAxios.get.mockRejectedValue(new Error('Failed to fetch'));
await expect(fetchProducts()).rejects.toThrow('Failed to fetch');
});
});
"""
### 5.2. Integration Tests
**Standard:** Write integration tests to verify the interaction between your React components, Recoil state, and API endpoints.
**Do This:** Use testing libraries like React Testing Library or Cypress to simulate user interactions and verify that the UI updates correctly based on API responses. Mock API responses to ensure consistent test results.
**Don't Do This:** Rely solely on unit tests, which may not catch integration issues between different parts of your application.
### 5.3. End-to-End Tests
**Standard:** Write end-to-end tests to verify the entire application flow, from the user interface to the backend API.
**Do This:** Use testing frameworks like Cypress or Playwright to automate browser interactions and verify that the application functions correctly from end to end. Test different user scenarios and edge cases to ensure application robustness.
**Don't Do This:** Skip end-to-end tests, which could lead to undetected issues in the overall application flow. Write brittle tests that are prone to failure due to minor UI changes.
By adhering to these coding standards, you can build robust, maintainable, and secure Recoil applications that effectively integrate with external APIs. Always refer to the official Recoil documentation for the latest features and best practices.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Deployment and DevOps Standards for Recoil This document outlines the coding standards for Deployment and DevOps related tasks in Recoil projects. Adhering to these guidelines ensures maintainability, performance, scalability, and security throughout the Software Development Life Cycle (SDLC). ## 1. Build Processes and CI/CD ### 1.1 Building the Application **Standard:** Use modern build tools (e.g., Webpack, Parcel, esbuild or Vite) optimized for Recoil projects. **Why:** These tools provide essential features like tree shaking, code splitting, and minification, significantly improving load times and reducing bundle sizes. With correct configuration, these tools will work well with Recoil. **Do This:** * Configure your build process to leverage tree shaking to remove unused Recoil code. * Use code splitting to logically divide your application into smaller, manageable chunks. * Minify and compress the output for production builds. * Ensure environment variables are properly handled during build time. **Don't Do This:** * Skip minification or compression for production deployments. * Manually manage dependencies; always utilize a package manager (npm, yarn, pnpm). * Include development-specific code or large debugging libraries in production builds. **Example (Vite Configuration):** """javascript // vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], build: { sourcemap: false, // Disable source maps in production minify: 'terser', // Use Terser for minification rollupOptions: { output: { // Manual chunking to optimize loading manualChunks: { vendor: ['react', 'react-dom', 'recoil'], // Common libraries // Other chunks as appropriate for your app }, }, }, }, define: { 'process.env': {} //Required for some libraries. }, }) """ **Anti-Pattern:** Neglecting to configure "build" options in your bundler (e.g., Webpack, Parcel). ### 1.2 Continuous Integration and Continuous Deployment (CI/CD) **Standard:** Implement a robust CI/CD pipeline using platforms like Jenkins, CircleCI, GitHub Actions, GitLab CI or Azure DevOps. **Why:** CI/CD automates the build, testing, and deployment processes, reducing human error and ensuring consistent and reliable deployments. Since Recoil state management impacts most of your components, frequent and automated testing become more relevant. **Do This:** * Integrate automated tests (unit, integration, and end-to-end) into the pipeline. * Automate the builds of your Recoil application with each commit or pull request. * Use environment-specific configuration for different stages (development, staging, production). * Implement rollback strategies for failed deployments. * Monitor build and deployment metrics to quickly identify and resolve issues. * Use tools that support branch-based deployment strategies (e.g., Gitflow). For example, automatically deploy to a staging environment upon a push to "develop" branch. **Don't Do This:** * Deploy directly from local machines to production environments. * Skip automated testing in your CI/CD pipeline. * Store sensitive data (e.g., API keys, database passwords) directly in the codebase. **Example (GitHub Actions):** """yaml # .github/workflows/deploy.yml name: Deploy to Production on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18' # Use a supported version cache: 'npm' - name: Install dependencies run: npm install - name: Run tests run: npm test # Or yarn test - name: Build application run: npm run build # Or yarn build env: API_ENV_VARIABLE: ${{ secrets.API_ENV_VARIABLE }} - name: Deploy to Production run: | # Example: Deploy to AWS S3 & CloudFront aws s3 sync ./dist s3://your-production-bucket aws cloudfront create-invalidation --distribution-id YOUR_CLOUDFRONT_DISTRIBUTION_ID --paths '/*' env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: your-aws-region """ **Anti-Pattern:** Manual deployments or insufficient test coverage in the CI/CD pipeline. ### 1.3 Environment Variables **Standard:** Manage environment-specific configurations and constants using environment variables. **Why:** Environment variables allow you to modify application behavior without changing the source code, enhancing flexibility, security, and portability. **Do This:** * Utilize a framework (e.g., ".env" files with "dotenv" package in development, environment variables in production). * Use tools like "cross-env" to manage environment variable settings across operating systems and CI/CD environments. * Ensure sensitive information is stored securely (e.g., using secret management services AWS Secrets Manager, Azure Key Vault, HashiCorp Vault). * Define clear naming conventions for environment variables. **Don't Do This:** * Hardcode sensitive information directly in your code. * Commit ".env" files containing sensitive data to version control. * Expose environment variables directly to the client-side code unless absolutely necessary. (Client-side environment variables should be prefixed with "PUBLIC_" or similar according to your build tool's best practice). **Example (.env file with dotenv):** """javascript // .env API_URL=https://api.example.com RECOIL_PERSISTENCE_KEY=my-app-state """ """javascript // webpack.config.js const Dotenv = require('dotenv-webpack'); module.exports = { plugins: [ new Dotenv(), ] }; // app.js const apiUrl = process.env.API_URL; // Access the API URL from the environment """ **Anti-Pattern:** Committing ".env" files to version control with sensitive information, or directly embedding sensitive credentials/keys in the application code. ## 2. Production Considerations Specific to Recoil ### 2.1 Atom Persistence State **Standard:** Employ proper persistence mechanisms for Recoil atom states, especially for applications requiring state preservation across sessions. **Why:** In some cases, you need to preserve state during page refreshes or even longer application sessions. Recoil itself does not provide persistence. This can improve User Experience. **Do This:** * Utilize libraries like ["recoil-persist"](https://github.com/ahabhgk/recoil-persist) for state persistence, taking extreme care to decide what data must persist and what should not. * Consider the size of the state you persist, as large states can increase load times. * For sensitive data, encrypt the persisted state or avoid persisting altogether. * Implement versioning for persisted states, ensuring compatibility across application updates. **Don't Do This:** * Persist sensitive data without proper encryption. * Persistently store extremely large amounts of data which will impact application load times * Forget to manage persistence versioning, potentially leading to data corruption during application upgrades. **Example (recoil-persist):** """javascript import { atom } from 'recoil'; import { recoilPersist } from 'recoil-persist'; const { persistAtom } = recoilPersist() export const userState = atom({ key: 'userState', default: { username: '', email: '', //Don't persist passwords or other sensitive information. }, effects_UNSTABLE: [persistAtom], }); """ **Anti-Pattern:** Persisting sensitive user data without encryption or storing excessively large datasets in persisted states. ### 2.2 Server-Side Rendering (SSR) with Recoil **Standard:** Handle Recoil state management correctly within SSR React applications. **Why:** SSR provides SEO benefits and enhances Time To First Byte (TTFB). Correct setup ensures the server renders with the appropriate initial state. **Do This:** * Utilize Recoil's "useRecoilSnapshot" hook in the server component to capture the state of the application. * Pass the snapshot as initial data to the client-side Recoil state. * Check the Recoil documentation and community resources for the most current strategies, as this area of SSR is actively evolving. **Don't Do This:** * Skip proper state hydration on the client side, leading to state inconsistencies between server-rendered and client-rendered content. * Overcomplicate data passing between server and client, potentially hindering performance. **Example (Next.js):** """javascript // pages/_app.js import { RecoilRoot } from 'recoil'; function MyApp({ Component, pageProps }) { return ( <RecoilRoot> <Component {...pageProps} /> </RecoilRoot> ); } export default MyApp; // pages/index.js import { useRecoilState } from 'recoil'; import { myAtom } from '../atoms'; function HomePage() { const [myValue, setMyValue] = useRecoilState(myAtom); return ( <div> <h1>My Value: {myValue}</h1> <button onClick={() => setMyValue(myValue + 1)}>Increment</button> </div> ); } export default HomePage; export async function getServerSideProps(context) { // Simulate fetching data from an API const initialValue = 42; return { props: { myAtom: initialValue }, }; } // atoms.js import { atom } from 'recoil'; export const myAtom = atom({ key: 'myAtom', default: 0, }); """ **Anti-Pattern:** Neglecting to initialize the client-side Recoil state with the server-rendered data, risking significant reconciliation issues and flickering UI elements. ### 2.3 Lazy Loading with Recoil **Standard:** Use Suspense and lazy-loaded components, particularly with Recoil selectors. **Why:** This reduces initial load times by deferring the loading of components and data until they are actually needed and improves application performance. **Do This:** * Enclose components that depend heavily on Recoil selectors within React's "<Suspense>" component. * Utilize "React.lazy" for code-splitting your application into smaller chunks. * Provide a fallback UI for components that are currently loading. * Consider preloading critical components or data for an improved user experience. **Don't Do This:** * Overuse Suspense, leading to frequent loading states and a poor user experience. * Neglect to handle errors that might occur during lazy loading. * Load all data on application startup; lazy load as needed. **Example:** """jsx import React, { Suspense, lazy } from 'react'; import { useRecoilValue } from 'recoil'; import { expensiveSelector } from './selectors'; const ExpensiveComponent = lazy(() => import('./ExpensiveComponent')); function MyComponent() { // Data from Recoil const data = useRecoilValue(expensiveSelector); return ( <Suspense fallback={<div>Loading...</div>}> {data ? <ExpensiveComponent data={data} /> : <div>No data available</div>} </Suspense> ); } export default MyComponent; """ **Anti-Pattern:** Unnecessary wrapping of small or insignificant components inside "<Suspense>" without code splitting. ### 2.4 Monitoring and Error Handling **Standard:** Implement robust monitoring and error-handling mechanisms to track application health and diagnose issues. **Why:** Proactive monitoring ensures prompt detection and resolution of problems, minimizing downtime and creating a robust user experience. **Do This:** * Use error tracking services like Sentry, Rollbar or Bugsnag to capture and analyze errors. * Implement logging throughout the application to provide detailed information about application behavior. * Monitor key performance indicators (KPIs) like response times, error rates and resource utilization. * Set up alerts to notify developers when critical issues arise. * Consider use Recoil's "useRecoilCallback"'s for handling errors gracefully. **Don't Do This:** * Ignore errors or rely solely on user reports to identify issues. * Store sensitive information in log files. * Fail to monitor application performance, potentially missing early warning signs of problems. **Example (Error Boundary and Sentry):** """jsx // ErrorBoundary.js import React, { Component } from 'react'; import * as Sentry from "@sentry/react"; class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service Sentry.captureException(error, { extra: errorInfo }); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong. Our engineers have been notified.</h1>; } return this.props.children; } } export default ErrorBoundary; """ """jsx import React from 'react'; import ErrorBoundary from './ErrorBoundary'; import MyComponent from './MyComponent'; function App() { return ( <ErrorBoundary> <MyComponent /> </ErrorBoundary> ); } export default App; """ **Anti-Pattern:** Ignoring errors or waiting for user reports to detect problems. ### 2.5 Recoil-Specific Performance Considerations: Selectors **Standard:** Optimize Recoil selector performance to minimize unnecessary re-renders and computations, this is an area where code can quickly become inefficient. **Why:** Selectors are the computed values in Recoil. Efficient selectors mean fewer re-renders and faster applications. **Do This:** * Use memoization techniques to cache selector results and avoid recomputation when inputs haven't changed. Recoil selectors are by default memoized. Don't break that. * Avoid complex or computationally expensive operations within selectors. If necessary defer these operations to a web worker. * Use "get" dependencies in selectors to only depend on atoms or other selectors that are absolutely necessary for the computed value. * Consider using "useRecoilValue" or "useRecoilValueLoadable" appropriately to only subscribe to the *values* you need. * Ensure testing includes consideration around Recoil selector performance. **Don't Do This:** * Perform side effects inside selectors. Selectors should be pure functions. * Create unnecessary dependencies in selectors. * Over-optimize selectors prematurely without profiling. * Over-use selectors when simple atom access would suffice. **Example (memoization built-in):** """javascript import { selector, atom } from 'recoil'; export const basePriceState = atom({ key: 'basePriceState', default: 10, }); export const discountRateState = atom({ key: 'discountRateState', default: 0.2, }); export const discountedPriceSelector = selector({ key: 'discountedPriceSelector', get: ({ get }) => { const basePrice = get(basePriceState); const discountRate = get(discountRateState); // This computation is only re-run when basePriceState / discountRateState change return basePrice * (1 - discountRate); }, }); """ **Anti-Pattern:** Complex computations inside selectors that trigger unnecessary re-renders when upstream data changes. Breaking the built-in memoization by introducing external state/variables or calling functions with side effects. Adhering to these Deployment and DevOps standards will significantly enhance the reliability, scalability, and performance of your Recoil applications, ultimately driving better user experiences and reducing operational overhead.
# Core Architecture Standards for Recoil This document outlines the core architectural standards for building applications with Recoil. It focuses on fundamental architectural patterns, project structure, and organization principles specific to Recoil, alongside modern approaches and best practices for maintainability, performance, and security. These standards are designed to be used as a guide for developers and as context for AI coding assistants. ## 1. Project Structure and Organization A well-structured project is crucial for maintainability and scalability. Consistency in project layout simplifies navigation, understanding, and collaboration. ### 1.1 Standard Directory Structure * **Do This:** Adopt a feature-based directory structure to group related components, atoms, and selectors. """ src/ ├── components/ │ ├── FeatureA/ │ │ ├── FeatureA.jsx │ │ ├── FeatureA.styles.js │ │ └── atoms.js │ ├── FeatureB/ │ │ ├── FeatureB.jsx │ │ ├── FeatureB.styles.js │ │ └── atoms.js ├── app/ // Application-level components and logic │ ├── App.jsx │ └── globalAtoms.js // Global atoms if absolutely necessary ├── utils/ │ ├── api.js │ └── helpers.js ├── recoil/ // Dedicate a directory for grouped Recoil atoms and selectors at the domain level. │ ├── user/ │ │ ├── user.atoms.js │ │ ├── user.selectors.js │ │ └── user.types.js │ ├── data/ │ │ ├── data.atoms.js │ │ └── data.selectors.js ├── App.jsx └── index.jsx """ * **Don't Do This:** Spread Recoil atoms and selectors across various directories unrelated to the feature they support. * **Why:** Feature-based organization improves modularity, reusability, and code discoverability. It simplifies dependency management and reduces the risk of naming conflicts. Grouping Recoil state with the associated feature promotes encapsulation. ### 1.2 Grouping Recoil definitions * **Do This:** Group state definitions into specific domains. """javascript // src/recoil/user/user.atoms.js import { atom } from 'recoil'; export const userState = atom({ key: 'userState', default: { id: null, name: '', email: '' }, }); """ """javascript // src/recoil/user/user.selectors.js import { selector } from 'recoil'; import { userState } from './user.atoms'; export const userNameSelector = selector({ key: 'userNameSelector', get: ({ get }) => { const user = get(userState); return user.name; }, }); """ * **Don't Do This:** Declare Recoil atoms and selectors directly within components or scattered across the project without any organizational structure. * **Why:** Grouping by domain enhances readability, simplifies state management by association, and improves maintainability. It makes it easier to find all state associated with a particular entity in your application ## 2. Atom and Selector Definition Properly defining atoms and selectors is crucial for performance and data flow clarity. ### 2.1 Atom Key Naming Conventions * **Do This:** Use descriptive and unique atom keys. The suggested pattern is "<feature>State". """javascript // Correct const userState = atom({ key: 'userState', default: { id: null, name: '', email: '' }, }); """ * **Don't Do This:** Use generic or ambiguous keys like "itemState" or "data". * **Why:** Unique keys prevent naming collisions and make debugging easier. Descriptive names improve code readability and understanding. ### 2.2 Selector Key Naming Conventions * **Do This:** Use descriptive and unique selector keys. The suggested pattern is "<feature><ComputedProperty>Selector". """javascript const userNameSelector = selector({ key: 'userNameSelector', get: ({ get }) => { const user = get(userState); return user.name; }, }); """ * **Don't Do This:** Use generic or ambiguous keys like "getValue" or "processData". * **Why:** Just as with atoms, unique keys prevent naming collisions and ease debugging. Descriptive names improve code readability and understanding. ### 2.3 Atom Default Values * **Do This:** Set meaningful default values for atoms. Use "null", empty strings, or appropriate initial states. """javascript const userState = atom({ key: 'userState', default: { id: null, name: '', email: '' }, }); """ * **Don't Do This:** Leave atoms undefined or use generic "undefined" as a default value without considering the implications. * **Why:** Meaningful default values prevent unexpected behavior and make the application more predictable. Prevents the need for null checks throughout your application, especially when TypeScript is employed. ### 2.4 Selector Purity and Memoization * **Do This:** Ensure selectors are pure functions: they should only depend on their inputs (Recoil state) and should not have side effects. """javascript const userNameSelector = selector({ key: 'userNameSelector', get: ({ get }) => { const user = get(userState); return user.name; }, }); """ * **Don't Do This:** Introduce side effects inside selectors (e.g., making API calls, updating external state). Avoid mutating the state within a get. * **Why:** Pure selectors ensure predictable behavior and enable Recoil's built-in memoization, improving performance by avoiding unnecessary re-computations. This is foundational to Recoil's design. ### 2.5 Asynchronous Selectors * **Do This:** Employ asynchronous selectors for fetching data or performing long-running computations. Ensure proper error handling. """javascript import { selector } from 'recoil'; import { fetchUser } from '../utils/api'; // Assume this is an API call. import { userIdState } from './user.atoms'; export const userProfileSelector = selector({ key: 'userProfileSelector', get: async ({ get }) => { const userId = get(userIdState); try { const user = await fetchUser(userId); // Asynchronous operation return user; } catch (error) { console.error("Error fetching user profile:", error); return null; // Handle the error appropriately. An Error Boundary would be better in some cases. } }, }); """ * **Don't Do This:** Perform synchronous, blocking operations within selectors, especially network requests. Neglect error handling in asynchronous selectors. * **Why:** Asynchronous selectors prevent the UI from freezing during long operations. Proper error handling ensures application stability. Utilize "Suspense" components to handle loading states gracefully. ## 3. Component Integration and Usage Components should interact with Recoil state in a clear, predictable, and efficient manner. ### 3.1 Using "useRecoilState" * **Do This:** Use "useRecoilState" for components that need to both read and modify an atom's value. """jsx import React from 'react'; import { useRecoilState } from 'recoil'; import { userNameState } from '../recoil/user/user.atoms'; function UsernameInput() { const [username, setUsername] = useRecoilState(userNameState); const handleChange = (event) => { setUsername(event.target.value); }; return ( <input type="text" value={username} onChange={handleChange} /> ); } """ * **Don't Do This:** Use "useRecoilState" in components that only need to read the value. Use "useRecoilValue" instead for read-only access. * **Why:** "useRecoilState" provides both read and write access. When only reading is needed, "useRecoilValue" is more performant because it avoids unnecessary subscriptions that handle potential writes. ### 3.2 Using "useRecoilValue" * **Do This:** Use "useRecoilValue" for components that only need to read the atom's or selector's value. """jsx import React from 'react'; import { useRecoilValue } from 'recoil'; import { userNameSelector } from '../recoil/user/user.selectors'; function UsernameDisplay() { const username = useRecoilValue(userNameSelector); return ( <div> Welcome, {username}! </div> ); } """ * **Don't Do This:** Reach for "useRecoilState" without evaluating if you only need the value. * **Why:** "useRecoilValue" optimizes rendering by only subscribing to value changes, reducing unnecessary re-renders. ### 3.3 Using "useSetRecoilState" * **Do This:** Use "useSetRecoilState" when a component only needs to update an atom's value without directly reading it. Best practice for event handlers or callbacks. """jsx import React from 'react'; import { useSetRecoilState } from 'recoil'; import { userEmailState } from '../recoil/user/user.atoms'; function EmailUpdate({ newEmail }) { const setEmail = useSetRecoilState(userEmailState); const handleClick = () => { setEmail(newEmail); }; return ( <button onClick={handleClick}>Update Email</button> ); } """ * **Don't Do This:** Overuse "useRecoilState" when only setting the state is required. * **Why:** "useSetRecoilState" only provides the "set" function, optimizing performance by avoiding unnecessary subscriptions to the atom's value. ### 3.4 Avoiding Prop Drilling with Recoil * **Do This:** Prefer using Recoil to manage state that is deeply nested or needed in multiple disconnected components, instead of passing data through props. * **Don't Do This:** Pass data through multiple layers of components just to get it to a specific child that requires it. Prop drilling increases coupling and makes refactoring difficult. * **Why:** Recoil allows direct access to the state from any component, eliminating the need for prop drilling and simplifying component structure. This greatly enhances modularity and maintainability. ### 3.5 Encapsulation of Recoil logic * **Do This:** Create custom hooks to wrap Recoil logic for a specific feature or component. """jsx // src/hooks/useUser.js import { useRecoilState, useRecoilValue } from 'recoil'; import { userState, userNameSelector } from '../recoil/user/user.atoms'; export const useUser = () => { const [user, setUser] = useRecoilState(userState); const userName = useRecoilValue(userNameSelector); return { user, setUser, userName }; }; """ * **Don't Do This:** Directly use "useRecoilState", "useRecoilValue", or "useSetRecoilState" within components without abstraction. * **Why:** Custom hooks promote reusability, improve code readability, and encapsulate Recoil-specific logic, making components cleaner and easier to understand. ## 4. Performance Optimization Optimizing Recoil applications involves minimizing unnecessary re-renders and computations. ### 4.1 Atom Families * **Do This:** Use atom families to manage collections of related state with dynamic keys. """javascript import { atomFamily } from 'recoil'; const todoItemState = atomFamily({ key: 'todoItemState', default: (id) => ({ id: id, text: '', isComplete: false, }), }); """ * **Don't Do This:** Create individual atoms for each item in a collection, especially for large datasets. * **Why:** Atom families efficiently manage collections by creating atoms on demand, reducing memory consumption and improving performance. ### 4.2 Memoization and Selectors * **Do This:** Leverage Recoil's built-in memoization by using selectors to derive values from atoms. """javascript import { selector } from 'recoil'; import { cartItemsState } from './cartAtoms'; export const cartTotalSelector = selector({ key: 'cartTotalSelector', get: ({ get }) => { const items = get(cartItemsState); return items.reduce((total, item) => total + item.price * item.quantity, 0); }, }); """ * **Don't Do This:** Perform complex computations directly within components, causing unnecessary re-renders. * **Why:** Memoization prevents redundant computations by caching selector results and only recomputing when dependencies change, improving performance. ### 4.3 Avoiding Unnecessary Re-renders * **Do This:** Ensure components only subscribe to the specific atoms or selectors they need. Avoid subscribing to entire state objects when only a portion is required. * **Don't Do This:** Subscribe to large, complex atoms in components that only need a small subset of the data. * **Why:** Reducing subscriptions minimizes the number of components that re-render when state changes, improving application performance. ### 4.4 Using "useRecoilCallback" * **Do This:** Use "useRecoilCallback" for complex state updates that require access to multiple atoms, especially when triggering updates based on current state. Consider debouncing or throttling updates within the callback for performance optimization. * **Don't Do This:** Perform multiple independent state updates in response to a single event, as this can trigger multiple re-renders. * **Why:** "useRecoilCallback" provides a way to batch state updates and ensure that changes are applied consistently, improving performance and preventing race conditions. ## 5. Error Handling and Debugging Effective error handling and debugging practices are essential for application stability and maintainability. ### 5.1 Error Boundaries * **Do This:** Wrap components that interact with Recoil state with error boundaries to catch and handle errors gracefully. """jsx import React, { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { useRecoilValue } from 'recoil'; import { userProfileSelector } from '../recoil/user/user.selectors'; function UserProfile() { const userProfile = useRecoilValue(userProfileSelector); return ( <div> {userProfile ? ( <> <h1>{userProfile.name}</h1> <p>{userProfile.email}</p> </> ) : ( <p>Loading user profile...</p> )} </div> ); } function ErrorFallback({ error, resetErrorBoundary }) { return ( <div role="alert"> <p>Something went wrong:</p> <pre>{error.message}</pre> <button onClick={resetErrorBoundary}>Try again</button> </div> ); } function UserProfileContainer() { return ( <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { // Reset the state or perform other error recovery tasks }} > <Suspense fallback={<p>Loading profile...</p>}> <UserProfile /> </Suspense> </ErrorBoundary> ); } """ * **Don't Do This:** Allow errors to propagate unchecked, potentially crashing the application. * **Why:** Error boundaries prevent application crashes by catching errors and providing fallback UI, improving user experience. Wrap your components using selectors that can potentially error (e.g. with external API calls) in a Suspense component to easily manage the loading state. ### 5.2 Logging and Debugging Tools * **Do This:** Utilize the Recoil DevTools extension to inspect and debug Recoil state changes. Add logging statements to track state updates and identify potential issues. * **Don't Do This:** Rely solely on console logs for debugging. * **Why:** Recoil DevTools provides a powerful interface for inspecting state, tracking changes over time, and identifying performance bottlenecks. ### 5.3 Centralized Error Handling * **Do This:** Implement a centralized error handling mechanism to catch and report errors consistently across the application. * **Don't Do This:** Handle errors in an ad-hoc manner, leading to inconsistent error reporting and difficulty in diagnosing issues. * **Why:** Centralized error handling provides a unified approach for managing errors, simplifying error reporting and making it easier to identify and resolve issues. ## 6. Testing Writing tests for Recoil applications is crucial for ensuring code quality and reliability. ### 6.1 Unit Testing Atoms and Selectors * **Do This:** Write unit tests for atoms and selectors to verify their behavior and ensure they produce the expected results. """javascript import { renderHook, act } from '@testing-library/react-hooks'; import { useRecoilState, useRecoilValue } from 'recoil'; import { userNameState, userNameSelector } from '../recoil/user/user.atoms'; import { testScheduler } from 'rxjs'; describe('Recoil State Tests', () => { it('should update userNameState correctly', () => { const { result } = renderHook(() => useRecoilState(userNameState)); const [name, setName] = result.current; expect(name).toBe(''); // Default value act(() => { setName('John Doe'); }); const [updatedName] = result.current; expect(updatedName).toBe('John Doe'); }); it('userNameSelector should return the correct username', () => { const { result } = renderHook(() => useRecoilValue(userNameSelector)); expect(result.current).toBe(''); }); }); """ * **Don't Do This:** Neglect unit testing atoms and selectors, leading to potential state management issues and unexpected behavior. * **Why:** Unit tests provide confidence in the correctness of Recoil state management logic, preventing bugs and improving application stability. Tools like "renderHook" from "@testing-library/react-hooks" allow you to unit test custom hooks that utilize Recoil's state management, but note that you will need to wrap them in a "RecoilRoot". ### 6.2 Integration Testing Components * **Do This:** Write integration tests for components that interact with Recoil state to ensure they render correctly and update state as expected. * **Don't Do This:** Skip integration testing components, leading to potential rendering or state update issues. * **Why:** Integration tests verify that components work correctly with Recoil state, ensuring the application behaves as expected from a user perspective. ### 6.3 Mocking Dependencies * **Do This:** Mock external dependencies such as API calls in selectors to isolate the testing environment and prevent relying on external resources. * **Don't Do This:** Directly call external APIs in tests, making tests dependent on external services and flaky. * **Why**: Mocking dependencies allows for reliable and reproducible tests, preventing external factors from causing test failures and speeding up the testing process. Recoil's testing utilities facilitate the mocking and overriding of atom and selector values during tests, providing a controlled testing environment. These architectural standards provide a solid foundation for building scalable, maintainable, and performant Recoil applications. Adhering to these guidelines will help development teams create high-quality code and deliver exceptional user experiences.
# Component Design Standards for Recoil This document outlines the coding standards for component design when using Recoil. These standards promote reusable, maintainable, and performant components. They are designed to be specific to Recoil, leveraging its unique features and addressing common pitfalls. ## 1. Component Architecture and Structure ### 1.1. Atom Composition and State Management **Standard:** Decompose complex component state into smaller, manageable atoms. **Do This:** """javascript // Bad: Single atom for everything const complexStateAtom = atom({ key: 'complexState', default: { name: '', age: 0, address: { street: '', city: '', }, // ...many more properties }, }); // Good: Separate atoms for different aspects of state const nameState = atom({ key: 'nameState', default: '' }); const ageState = atom({ key: 'ageState', default: 0 }); const streetState = atom({ key: 'streetState', default: '' }); const cityState = atom({ key: 'cityState', default: '' }); """ **Don't Do This:** Pile all component data into a single, monolithic atom. This reduces re-rendering efficiency, increases complexity, and hinders reusability. **Why:** Smaller atoms lead to more granular re-renders when using "useRecoilValue" or "useRecoilState", improving performance. They also enhance modularity and testability. Separate atoms also simplify the process of persisting certain parts of the state, while omitting others if necessary. **Example:** """javascript import { useRecoilState } from 'recoil'; import { nameState, ageState } from './atoms'; function UserProfileForm() { const [name, setName] = useRecoilState(nameState); const [age, setAge] = useRecoilState(ageState); return ( <div> <label>Name: <input value={name} onChange={(e) => setName(e.target.value)} /></label> <label>Age: <input value={age} type="number" onChange={(e) => setAge(Number(e.target.value))} /></label> </div> ); } """ ### 1.2. Selector Usage for Derived Data **Standard:** Derive component-specific data from atoms using selectors. Avoid performing complex calculations or transformations directly within the component. **Do This:** """javascript // Create selector to derive a formatted age string. import { selector } from 'recoil'; import { ageState } from './atoms'; export const formattedAgeState = selector({ key: 'formattedAgeState', get: ({ get }) => { const age = get(ageState); return "Age: ${age}"; }, }); // Good component example : displaying derived data import { useRecoilValue } from 'recoil'; import { formattedAgeState } from './selectors'; function UserAgeDisplay() { const formattedAge = useRecoilValue(formattedAgeState); return <div>{formattedAge}</div>; } """ **Don't Do This:** Perform calculations directly within components, or derive intermediate results via "useRecoilValue". Selectors are memoized and cached. **Why:** Selectors improve performance by memoizing derived data. Components only re-render when the underlying atom values change. They also encapsulate complex logic and improve testability. By using selectors, you avoid unnecessary computations and re-renders when the upstream atom values have not changed. Selectors also allow us to do asynchronous operations, and computations of other atoms. ### 1.3. Component Decoupling with "useRecoilValue" **Standard:** Prefer "useRecoilValue" for components that only *read* Recoil state. Use "useRecoilState" only when the component *needs to update* the state. **Do This:** """javascript import { useRecoilValue } from 'recoil'; import { nameState } from './atoms'; function NameDisplay() { const name = useRecoilValue(nameState); return <div>Name: {name}</div>; } """ **Don't Do This:** Use "useRecoilState" when you only need to read the value. This can lead to unnecessary re-renders if the component doesn't actually modify the state. **Why:** "useRecoilValue" creates a read-only dependency. The component will only re-render when the atom value changes. "useRecoilState" provides both the value and a setter function, meaning the component might re-render when the setter function changes, even if you aren't using it. This approach optimizes rendering performance. ### 1.4. Asynchronous Selectors for Data Fetching **Standard:** Use asynchronous selectors for fetching data, managing loading states, and handling errors. **Do This:** """javascript // Asynchronous selector for fetching user data import { selector } from 'recoil'; export const userFetchState = selector({ key: 'userFetchState', get: async () => { try { const response = await fetch('/api/user'); const data = await response.json(); return data; } catch (error) { console.error("Error fetching user:", error); return { error: error.message }; // Consider a dedicated error state } }, }); // Component example: Displaying or loading user data import { useRecoilValue } from 'recoil'; import { userFetchState } from './selectors'; function UserProfile() { const user = useRecoilValue(userFetchState); if (!user) { return <div>Loading...</div>; } if (user.error) { return <div>Error: {user.error}</div>; } return ( <div> <h1>{user.name}</h1> <p>Email: {user.email}</p> </div> ); } """ **Don't Do This:** Fetch data directly in components using "useEffect" and manually manage the loading state with local React state. Recoil elegantly handles loading, errors and caching through selectors. **Why:** Asynchronous selectors provide a clean and declarative way to handle data fetching. They encapsulate the data fetching logic and improve code organization. They can also be cached. They automatically handle the loading state. Error handling can be done within the selector, leading to cleaner component code. ### 1.5. Using "useRecoilCallback" for Complex State Updates **Standard:** Use "useRecoilCallback" for complex state updates, side effects, or interactions. **Do This:** """javascript import { useRecoilCallback } from 'recoil'; import { nameState, ageState } from './atoms'; function UserProfileActionButtons() { const updateUser = useRecoilCallback(({ set, snapshot }) => async (newName, newAge) => { // Simulate API call await new Promise(resolve => setTimeout(resolve, 500)); // Atomic updates using the snapshot of other atom values set(nameState, newName); set(ageState, newAge); // Example: Use snapshot to read the current name before update const currentName = await snapshot.getPromise(nameState); console.log("Previous name was: ${currentName}"); }, []); return ( <div> <button onClick={() => updateUser('Jane Doe', 31)}>Update User</button> </div> ); } """ **Don't Do This:** Directly manipulate multiple atoms within a component using multiple "set" calls without "useRecoilCallback". Also, avoid creating closures over Recoil state within event handlers. **Why:** "useRecoilCallback" allows you to access or set multiple atoms atomically and perform side effects in a controlled manner. It prevents race conditions and ensures data consistency. It also addresses the stale closure problem that can occur when using event handlers with Recoil state directly. It also allows reading of previous state synchronously at the time of running, using the "snapshot". ## 2. Component Reusability ### 2.1. Parameterized Atoms for Reusable Components **Standard:** Create atoms that accept parameters to create more reusable component instances. **Do This:** """javascript // Parameterized atom factory: example with a key that accepts props. import { atom } from 'recoil'; const makeItemAtom = (itemId) => atom({ key: "item-${itemId}", default: '', }); function ItemDisplay({ itemId }) { const [item, setItem] = useRecoilState(makeItemAtom(itemId)); return ( <div> Item {itemId}: <input value={item} onChange={(e) => setItem(e.target.value)} /> </div> ); } """ **Don't Do This:** Define unique atoms for each instance of a component, leading to code duplication. Instead of making a parameterizable atom family. Instead of making atoms inside components (violates the rules of hooks.) **Why:** This promotes reusability by allowing a single component definition to manage multiple independent pieces of state based on the provided parameters. It avoids redundant code. ### 2.2. Higher-Order Components (HOCs) and Render Props with Recoil **Standard:** Use HOCs or render props patterns to share Recoil-related logic between components. **Do This:** """javascript // HOC example: Enhancing a component with Recoil state import { useRecoilValue } from 'recoil'; import { someState } from './atoms'; const withRecoilState = (WrappedComponent) => { return function WithRecoilState(props) { const stateValue = useRecoilValue(someState); return <WrappedComponent {...props} recoilValue={stateValue} />; }; }; // Use the HOC: function MyComponent({ recoilValue }) { return <div>State Value: {recoilValue}</div>; } export default withRecoilState(MyComponent); """ **Don't Do This:** Duplicate Recoil hooks logic in multiple components that need similar state management. **Why:** HOCs and render props enable code reuse and separation of concerns. Centralizing the Recoil parts make components more maintainable. ### 2.3. Atom Families for Dynamic State Management **Standard:** Using atom families where multiple instances of components manage state, such as with lists. """javascript import { atomFamily, useRecoilState } from 'recoil'; const itemFamily = atomFamily({ key: 'itemFamily', default: '', }); function Item({ id }) { const [text, setText] = useRecoilState(itemFamily(id)); return ( <div> <input type="text" value={text} onChange={(e) => setText(e.target.value)} /> </div> ); } function List() { const [items, setItems] = useState([]); const add = () => { setItems([...items, items.length]); } return ( <div> <button onClick={add}>Add</button> <ul> {items.map((id) => ( <li key={id}> <Item id={id} /> </li> ))} </ul> </div> ); } """ **Don't Do This:** Manually manage lists of atoms, or creating large data structures holding atom state. **Why:** Allows scalable state management that's performant. Prevents stale closures. ## 3. Component Performance ### 3.1. Selective Rendering with "useRecoilValue" and "useRecoilState" **Standard:** Use "useRecoilValue" when the component *only reads* the state and "useRecoilState" only when the component *modifies* the state. **Do This:** (See section 1.3 for complete examples) **Don't Do This:** Always use "useRecoilState" without needing the setter function, as it can lead to unnecessary re-renders. **Why:** "useRecoilValue" creates a read-only dependency, reducing re-renders. ### 3.2. Selector Memoization **Standard:** Leverage the built-in memoization of Recoil selectors to minimize redundant computations. **Do This:** (As shown in 1.2; selectors are memoized by default) **Don't Do This:** Assume that selectors are *not* memoized and attempt to implement custom memoization logic. **Why:** Recoil selectors automatically cache their results. The cache invalidates when the dependencies of the selector change, so no
# State Management Standards for Recoil This document outlines the coding standards and best practices for state management using Recoil, ensuring maintainable, performant, and scalable applications. It focuses on how to effectively manage application data flow and reactivity with Recoil's core concepts. ## 1. Core Principles of Recoil State Management ### 1.1. Data-Flow Graph **Standard:** Model application state as a data-flow graph consisting of atoms (units of state) and selectors (derived state). **Do This:** * Define atoms to represent the source of truth for pieces of your application state. * Use selectors to derive computed values from atoms and other selectors. This approach centralizes logic and ensures consistency. **Don't Do This:** * Directly mutate atom values from components. * Duplicate derived logic in multiple components; always use selectors for computed data. **Why:** A data-flow graph promotes a unidirectional data flow, making it easier to understand how state changes propagate through the application. Centralizing logic enhances maintainability and prevents inconsistencies. **Example:** """javascript import { atom, selector } from 'recoil'; // Atom representing user input const userInputState = atom({ key: 'userInputState', default: '', }); // Selector deriving the character count from the user input const characterCountState = selector({ key: 'characterCountState', get: ({ get }) => { const input = get(userInputState); return input.length; }, }); export { userInputState, characterCountState }; """ ### 1.2. Unidirectional Data Flow **Standard:** Enforce unidirectional data flow by updating atoms exclusively through explicit actions. **Do This:** * Update atom values by using "set" from "useRecoilState" or "useSetRecoilState" within event handlers or asynchronous operations. * Dispatch actions that encapsulate state updates, especially when updates are complex or involve multiple atoms. **Don't Do This:** * Directly modify state outside the boundaries of designated update functions. * Create cycles in your data-flow graph by having selectors depend on each other in a circular manner. **Why:** Unidirectional data flow simplifies debugging and provides predictable state transitions. It also aligns with the principles of functional programming, reducing side effects. **Example:** """javascript import { useRecoilState } from 'recoil'; import { userInputState } from './atoms'; function InputComponent() { const [input, setInput] = useRecoilState(userInputState); const handleChange = (event) => { setInput(event.target.value); // Using set to update the atom }; return ( <input type="text" value={input} onChange={handleChange} /> ); } """ ### 1.3. Immutability **Standard:** Treat Recoil atoms as immutable data structures. When updating an atom, create a new object or array rather than modifying the existing one in place. **Do This:** * Use the spread operator ("...") for objects and arrays to create new instances when updating state. * Adopt libraries like Immer to simplify immutable updates, especially for complex data structures. * Use the "useImmerRecoilState" hook from "recoil-immer-state" for simplified state management with Immer. **Don't Do This:** * Use methods that directly mutate arrays like "push", "pop", "splice" without creating a new array. * Modify properties of objects directly (e.g., "state.property = newValue"). **Why:** Immutability enhances predictability, simplifies debugging (time-travel debugging becomes easier), and enables efficient change detection. **Example (using spread operator):** """javascript import { atom, useRecoilState } from 'recoil'; const itemsState = atom({ key: 'itemsState', default: [], }); function ItemList() { const [items, setItems] = useRecoilState(itemsState); const addItem = (newItem) => { setItems([...items, newItem]); // create new array with the spread operator }; return ( <div> <button onClick={() => addItem({ id: Date.now(), text: 'New Item' })}> Add Item </button> <ul> {items.map((item) => ( <li key={item.id}>{item.text}</li> ))} </ul> </div> ); } """ **Example (using Immer):** """javascript import { atom } from 'recoil'; import { useImmerRecoilState } from 'recoil-immer-state'; const itemsState = atom({ key: 'itemsState', default: [], }); function ItemList() { const [items, updateItems] = useImmerRecoilState(itemsState); const addItem = (newItem) => { updateItems(draft => { draft.push(newItem); // Immer allows mutation within draft }); }; return ( <div> <button onClick={() => addItem({ id: Date.now(), text: 'New Item' })}> Add Item </button> <ul> {items.map((item) => ( <li key={item.id}>{item.text}</li> ))} </ul> </div> ); } """ ### 1.4. Atom Design **Standard:** Design atoms to represent granular pieces of state to optimize component re-renders. Strive for atoms that represent the minimal required state. **Do This:** * Break down large state objects into smaller, more specific atoms. * Use selector families to parameterize state and avoid creating multiple atoms for similar data. **Don't Do This:** * Store unrelated pieces of data in the same atom. * Create atoms for derived or computed values; use selectors instead. **Why:** Granular atoms minimize unnecessary re-renders, leading to improved performance. **Example:** """javascript import { atom } from 'recoil'; // Good: Separate atoms for different aspects of user data const userNameState = atom({ key: 'userNameState', default: '', }); const userEmailState = atom({ key: 'userEmailState', default: '', }); // Bad: All user data in one atom (leads to unnecessary re-renders) const userDataState = atom({ key: 'userDataState', default: { name: '', email: '', }, }); export {userNameState, userEmailState, userDataState}; """ ### 1.5. Selector Usage **Standard:** Utilize selectors to encapsulate derived state and perform data transformations. **Do This:** * Use selectors to compute values based on one or more atoms. * Employ selector families to create parameterized selectors when needing to derive state based on dynamic inputs. * Utilize asynchronous selectors for data fetching and transformations that require asynchronous operations. **Don't Do This:** * Perform complex logic directly within components; delegate it to selectors. * Expose raw atom values directly to components without necessary transformations or filtering. **Why:** Selectors promote code reusability, ensure consistency, and improve performance by caching derived values. **Example (Selector Family):** """javascript import { atom, selectorFamily } from 'recoil'; const todoListState = atom({ key: 'todoListState', default: [{id: 1, text: "Initial Todo", isComplete: false}] }); const todoItemSelector = selectorFamily({ key: 'todoItemSelector', get: (todoId) => ({ get }) => { const todoList = get(todoListState); return todoList.find((item) => item.id === todoId)}; }); """ ## 2. Advanced Recoil Patterns ### 2.1. Asynchronous Selectors **Standard:** Manage asynchronous data dependencies using asynchronous selectors. **Do This:** * Define selectors that use "async" functions to fetch data from APIs or perform other asynchronous operations. * Handle loading states and errors gracefully within the selector. **Don't Do This:** * Perform data fetching directly within components; use asynchronous selectors instead. * Ignore error cases when fetching data within asynchronous selectors. **Why:** Asynchronous selectors provide a clean and efficient way to manage asynchronous data dependencies, reducing the risk of race conditions and simplifying component logic. **Example:** """javascript import { atom, selector } from 'recoil'; const userIdState = atom({ key: 'userIdState', default: 1, }); const userProfileState = selector({ key: 'userProfileState', get: async ({ get }) => { const userId = get(userIdState); 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(); }, }); """ ### 2.2. Recoil Sync **Standard:** Utilize "recoil-sync" for persisting and synchronizing Recoil state across different sessions or devices. **Do This:** * Configure "recoil-sync" to persist specific atoms to local storage, session storage, or other storage mechanisms. * Implement strategies for handling conflicts and migrations when state structures change. **Don't Do This:** * Store sensitive data in local storage without encryption. * Neglect to handle versioning and backward compatibility when evolving state. **Why:** "recoil-sync" simplifies state persistence and synchronization, providing a seamless user experience across sessions and devices. **Example:** """javascript import { atom } from 'recoil'; import { recoilSync } from 'recoil-sync'; const textState = atom({ key: 'textState', default: '', }); const { useRecoilSync } = recoilSync({ store: localStorage, // Use localStorage for persistence }); function TextComponent() { const [text, setText] = useRecoilState(textState); useRecoilSync({ atom: textState, storeKey: 'text', // Key to store in localStorage }); const handleChange = (event) => { setText(event.target.value); }; return <input type="text" value={text} onChange={handleChange} />; } """ ### 2.3. Atom Families **Standard:** Use atom families when managing collections of similar state variables. **Do This:** * Define an atom family parameterized by a unique ID. * Access specific atom instances using the "useRecoilValue" hook. **Don't Do This:** * Create separate atoms for each item in a collection; use an atom family instead. * Oversimplify state management by using a single atom for all collection items. **Why:** Atom families allow you to dynamically create and manage state variables based on unique identifiers, improving scalability and maintainability. **Example:** """javascript import { atomFamily, useRecoilState } from 'recoil'; const todoItemState = atomFamily({ key: 'todoItemState', default: (id) => ({ id: id, text: '', isComplete: false, }), }); function TodoItem({ id }) { const [todo, setTodo] = useRecoilState(todoItemState(id)); const handleChange = (event) => { setTodo({ ...todo, text: event.target.value }); }; return <input type="text" value={todo.text} onChange={handleChange} />; } """ ## 3. Performance Optimization ### 3.1. Minimize Atom Updates **Standard:** Batch updates and avoid unnecessary state changes. **Do This:** * Use "useRecoilTransaction" to bundle multiple atom updates into a single transaction. * Implement debouncing or throttling to reduce the frequency of state updates. **Don't Do This:** * Trigger state updates on every keystroke or mouse movement without considering performance implications. * Update atoms unnecessarily; only update state when it actually changes. **Why:** Reducing the number of state updates minimizes component re-renders, resulting in improved application performance. **Example (using "useRecoilTransaction"):** """javascript import { atom, useRecoilState, useRecoilTransaction } from 'recoil'; const firstNameState = atom({ key: 'firstNameState', default: '', }); const lastNameState = atom({ key: 'lastNameState', default: '', }); function NameForm() { const [firstName, setFirstName] = useRecoilState(firstNameState); const [lastName, setLastName] = useRecoilState(lastNameState); const updateName = useRecoilTransaction(({ set }) => (newFirstName, newLastName) => { set(firstNameState, newFirstName); set(lastNameState, newLastName); }, [setFirstName, setLastName]); const handleUpdate = () => { updateName('John', 'Doe'); }; return ( <div> <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} placeholder="First Name" /> <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} placeholder="Last Name" /> <button onClick={handleUpdate}>Update Name</button> </div> ); } """ ### 3.2. Optimize Selector Logic **Standard:** Optimize selector logic to minimize computation time. **Do This:** * Memoize expensive computations within selectors. Leverage libraries like "lodash" or "ramda" for memoization. * Use selector dependencies wisely to prevent unnecessary recalculations. **Don't Do This:** * Perform complex or redundant computations within selectors without memoization. * Create selector dependencies that trigger frequent recalculations without a valid reason. **Why:** Optimizing selector logic prevents performance bottlenecks and ensures that derived state is computed efficiently. **Example:** """javascript import { atom, selector } from 'recoil'; import { memoize } from 'lodash'; const dataState = atom({ key: 'dataState', default: [], }); const processedDataState = selector({ key: 'processedDataState', get: ({ get }) => { const data = get(dataState); // Memoize the expensive processing function const processData = memoize((data) => { // Perform expensive data processing console.log("Processing Data") //This will only log when the underlying data changes return data.map((item) => item * 2); }); return processData(data); }, }); """ ### 3.3. Component Subscriptions **Standard:** Implement efficient component subscriptions to avoid unnecessary re-renders. **Do This:** * Use "useRecoilValue" to subscribe only to the specific atoms or selectors that a component requires. * Use "useRecoilCallback" for performing side effects within components. **Don't Do This:** * Subscribe components to atoms or selectors that are not needed for rendering. * Overuse "useRecoilState" when only reading a value is necessary; "useRecoilValue" is often more efficient. **Why:** Efficient subscriptions prevent unnecessary re-renders, improving component performance and overall application responsiveness. ## 4. Error Handling ### 4.1. Handling Selector Errors **Standard:** Properly handle errors within asynchronous selectors. **Do This:** * Use "try...catch" blocks within asynchronous selectors to catch errors that may occur during data fetching. * Return an error state or a default value when an error occurs, providing feedback to the user. **Don't ** * Leave async selectors unhandled, causing application crashes * Fail to provide user feedback concerning issues in the selector * Fail to log errors **Why:** Properly handling errors within asynchronous selectors guarantees application stability and offers crucial feedback to the user. **Example:** """javascript import { atom, selector } from 'recoil'; const userIdState = atom({ key: 'userIdState', default: 1, }); const userProfileState = selector({ key: 'userProfileState', get: async ({ get }) => { try { const userId = get(userIdState); 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); return { error: 'Failed to load user data' }; } }, }); """ ### 4.2 Handling Transaction Errors **Standard:** Handle errors during useRecoilTransaction **Do This:** * Implement a try/catch block within the actions performed during a useRecoilTransaction call. * Rollback states when you discover an error and can not complete the requested transaction * Provide user feedback **Don't Do This:** * Leave trasactions uncontrolled * Provide user inormation that could be harmful in the UI **Why:** Atomic and bulletproof transactions enable the creation of reliable state changes. """javascript import { atom, useRecoilState, useRecoilTransaction } from 'recoil'; const balanceState = atom({ key: 'balance', default: 100, }); const amountState = atom({ key: 'amount', default: 0, }); function TransactionComponent() { const [balance, setBalance] = useRecoilState(balanceState); const [amount, setAmount] = useRecoilState(amountState); const performTransaction = useRecoilTransaction( ({ set, get }) => (transferAmount) => { try { if (transferAmount <= 0) { throw new Error("Transfer amount must be positive"); } const currentBalance = get(balanceState); if (currentBalance < transferAmount) { throw new Error("Insufficient balance"); } set(balanceState, currentBalance - transferAmount); set(amountState, transferAmount); } catch (error) { // Rollback or handle error here console.error("Transaction failed:", error.message); // Optionally reset state or notify user } }, [balance, amount, set] ); const handleTransfer = () => { performTransaction(50); }; return ( <div> <div>Balance: {balance}</div> <div>Amount: {amount}</div> <button onClick={handleTransfer}>Transfer</button> </div> ); } export default TransactionComponent; """ ## 5. Code Style and Formatting ### 5.1. Naming Conventions **Standard:** Follow consistent naming conventions for atoms and selectors. **Do This:** * Use PascalCase for atom and selector keys (e.g., "UserNameState", "CharacterCountSelector"). * Suffix atom keys with "State" and selector keys with "Selector". **Why:** Consistent naming conventions improve code readability and maintainability. ### 5.2. File Structure **Standard:** Organize Recoil atoms and selectors into dedicated files. **Do This:** * Create a "state" directory to store all Recoil-related files. * Group related atoms and selectors into separate modules within the "state" directory. **Why:** Proper file structure improves code organization and makes it easier to locate and maintain state-related code. ### 5.3. Comments and Documentation **Standard:** Provide clear comments and documentation for atoms and selectors. **Do This:** * Add JSDoc-style comments to explain the purpose of each atom and selector. * Document any complex logic or dependencies within selectors. **Why:** Clear comments and documentation improve code understandability and facilitate collaboration among developers. By adhering to these standards, your Recoil code will be more maintainable, performant, and scalable.
# Performance Optimization Standards for Recoil This document outlines coding standards specifically focused on performance optimization when using Recoil. Following these standards will lead to more responsive, scalable, and maintainable applications. ## 1. Atom Design and Management ### 1.1 Atom Granularity * **Do This:** Strive for fine-grained atoms that represent the smallest unit of independent state needed by components. * **Don't Do This:** Create large, monolithic atoms that hold unrelated pieces of state. **Why:** Fine-grained atoms minimize unnecessary re-renders. When a component subscribes to a smaller atom, it only re-renders when that specific atom changes, not when unrelated data within a larger atom changes. **Example:** """javascript //Good: Fine-grained atoms import { atom } from 'recoil'; export const todoListState = atom({ key: 'todoListState', default: [], }); export const todoListFilterState = atom({ key: 'todoListFilterState', default: 'Show All', }); //Bad: Monolithic atom //Causes unnecessary renders when only filter changes or vice-versa export const todoListCombinedState = atom({ key: 'todoListCombinedState', default: { todos: [], filter: 'Show All', }, }); """ ### 1.2 Atom Family Usage * **Do This:** Utilize "atomFamily" when working with collections of similar data entities, where each entity needs its own independent state. * **Don't Do This:** Create separate individual atoms for each entity or rely on complex filtering within a single atom to manage individual entity state. **Why:** "atomFamily" provides a performant and scalable way to manage large numbers of atoms with efficient memory usage and optimized selector dependencies. Recoil internally optimizes access and updates to atom families. **Example:** """javascript import { atomFamily } from 'recoil'; const todoItemState = atomFamily({ key: 'todoItem', default: (id) => ({ id, text: '', isComplete: false, }), }); // Usage in a component: import { useRecoilState } from 'recoil'; function TodoItem({ id }) { const [todo, setTodo] = useRecoilState(todoItemState(id)); const onChange = (e) => { setTodo({ ...todo, text: e.target.value }); }; return <input value={todo.text} onChange={onChange} />; } """ ### 1.3 Limiting Atom Scope * **Do This:** Scope atoms to the smallest necessary part of the component tree using "RecoilRoot". * **Don't Do This:** Define all atoms at the top level of the application. **Why:** Scoping atoms reduces the potential for unintended side effects and improves performance by limiting the number of components that can subscribe to a given atom's changes. **Example:** """javascript // Only components within this SomeFeature component can access these states function SomeFeature() { return ( <RecoilRoot> <FeatureComponent1 /> <FeatureComponent2 /> </RecoilRoot> ) } """ ## 2. Selector Optimization ### 2.1 Memoization * **Do This:** Leverage the built-in memoization of Recoil selectors. Selectors automatically memoize their results based on their input dependencies. * **Don't Do This:** Attempt to manually memoize selector results. **Why:** Recoil provides a highly optimized memoization strategy that avoids unnecessary re-computation when dependencies haven't changed. Manual memoization can introduce errors and performance overhead. **Example:** """javascript import { selector } from 'recoil'; import { todoListState } from './atoms'; export const todoListStatsState = selector({ key: 'todoListStatsState', get: ({ get }) => { const todoList = get(todoListState); const totalNum = todoList.length; const totalCompletedNum = todoList.filter((item) => item.isComplete).length; const totalUncompletedNum = totalNum - totalCompletedNum; const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; return { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, }; }, }); """ In the example above, "todoListStatsState" will only re-compute its derived state when "todoListState" changes. ### 2.2 Avoiding Expensive Computations in Selectors * **Do This:** Keep selector computations lightweight and efficient. Offload expensive computations to background processes or use techniques like pagination or virtualization. * **Don't Do This:** Perform computationally intensive operations directly within selectors, as this can block the main thread and degrade UI responsiveness. **Why:** Selectors should be quick to execute. Long-running computations within selectors can cause noticeable delays in UI updates. **Example:** """javascript // Bad: computationally intensive operation inside a selector export const largeDataProcessingSelector = selector({ key: 'largeDataProcessingSelector', get: ({get}) => { const data = get(largeDatasetAtom); //Simulate expensive operation let result = 0; for (let i = 0; i < data.length; i++) { result += Math.sqrt(data[i]); } return result; }, }); //Good: offloading the computation or pre-computing it export const preComputedResultSelector = selector({ key: 'preComputedResultSelector', get: ({get}) => { return get(preComputedAtom); // The expensive operation has already been pre-computed } }); """ **Anti-pattern:** Performing network requests or database queries directly in selector "get" functions. These should be handled outside of selector logic, and the results stored in atoms. ### 2.3 Selector Families * **Do This:** Use "selectorFamily" to create selectors that accept parameters, enabling efficient retrieval of derived state for individual items in a collection. * **Don't Do This:** Retrieve all items from an atom and then filter or process them within a component, which can lead to unnecessary re-renders. **Why:** "selectorFamily" allows components to subscribe only to the specific derived state they need, minimizing re-renders and improving performance. **Example:** """javascript import { selectorFamily } from 'recoil'; import { todoListState } from './atoms'; export const todoItemSelector = selectorFamily({ key: 'todoItemSelector', get: (id) => ({ get }) => { const todoList = get(todoListState); return todoList.find((item) => item.id === id); }, }); // Usage in a component: import { useRecoilValue } from 'recoil'; function TodoItemDisplay({ id }) { const todo = useRecoilValue(todoItemSelector(id)); if (!todo) { return null; } return <div>{todo.text}</div>; } """ ### 2.4 Read-Only Selectors * **Do This:** Use "useRecoilValue" when you only need to read data from a selector and don't need to modify it. * **Don't Do This:** Use "useRecoilState" if you only intend to read the selector's value, as it creates an unnecessary setter function resulting in a performance overhead. **Why:** Using "useRecoilValue" for read-only access optimizes performance because it avoids creating a setter function and subscribing to unnecessary updates. **Example:** """javascript import { useRecoilValue } from 'recoil'; import { todoListStatsState } from './selectors'; function TodoListSummary() { const { totalNum, totalCompletedNum } = useRecoilValue(todoListStatsState); return ( <div> Total items: {totalNum}, Completed: {totalCompletedNum} </div> ); } """ ## 3. Component Optimization ### 3.1 Immutability * **Do This:** Treat state retrieved from Recoil atoms as immutable. Create new objects or arrays when modifying state, rather than mutating the existing ones. * **Don't Do This:** Directly modify objects or arrays stored in atoms. **Why:** Recoil relies on immutability to efficiently detect changes and trigger re-renders in only the necessary components. Mutating state can lead to unpredictable behaviour and missed updates. **Example:** """javascript // Good: Creating a new array import { useRecoilState } from 'recoil'; import { todoListState } from './atoms'; function AddTodoItem() { const [todoList, setTodoList] = useRecoilState(todoListState); const addItem = () => { const newItem = { id: Date.now(), text: 'New Item', isComplete: false }; setTodoList([...todoList, newItem]); // Creating a new array }; return <button onClick={addItem}>Add Item</button>; } // Bad: Mutating the existing array function BadAddTodoItem() { const [todoList, setTodoList] = useRecoilState(todoListState); const addItem = () => { const newItem = { id: Date.now(), text: 'New Item', isComplete: false }; todoList.push(newItem); //Mutating todoList - AVOID THIS! setTodoList(todoList); // Recoil won't detect/trigger updates reliably. }; return <button onClick={addItem}>Add Item</button>; } """ ### 3.2 Selective Rendering * **Do This:** Utilize "React.memo" or similar techniques (like "useMemo" for inline components) to prevent unnecessary re-renders of components that don't depend on Recoil state. * **Don't Do This:** Assume that Recoil automatically optimizes all re-renders. **Why:** Although Recoil optimizes state management, React component re-renders are still governed by React's rendering lifecycle. Explicit memoization can significantly reduce the workload on the virtual DOM. **Example:** """javascript import React from 'react'; import { useRecoilValue } from 'recoil'; import { todoListFilterState } from './atoms'; function FilterDisplay() { const filter = useRecoilValue(todoListFilterState); console.log("FilterDisplay Re-rendered!"); //check if re-rendering unnecessarily return <div>Current Filter: {filter}</div>; } export default React.memo(FilterDisplay); """ In this example, "FilterDisplay" will only re-render when "todoListFilterState" changes, preventing unnecessary re-renders even if its parent component re-renders. ### 3.3 Avoiding Inline Functions in Render * **Do This:** Move function declarations outside of the render function or use "useCallback" hook when passing functions as props to prevent components from re-rendering unnecessarily. * **Don't Do This:** Declare inline functions when passing them as props. **Why:** Inline functions create a new function instance on every render, causing the child component to re-render even if the function's logic remains the same. **Example:** """javascript //Good import React, { useCallback } from 'react'; function ParentComponent({ onAction }) { // Use useCallback to memoize the callback function const handleClick = useCallback(() => { onAction(); }, [onAction]); // Only re-create the function if onAction changes return <ChildComponent onClick={handleClick} />; } function ChildComponent({ onClick }) { console.log("Child rendered"); return (<button onClick={onClick}>Click Me</button>) } export default ChildComponent // Bad: creates new function instances every render function BadParentComponent({ onAction }) { return <ChildComponent onClick={() => onAction()} />; } """ ### 3.4 Batch Updates * **Do This:** Use "batch" from "recoil" when performing multiple state updates in quick succession to avoid unnecessary intermediate re-renders. * **Don't Do This:** Trigger multiple state updates sequentially without batching. **Why:** Batched updates allow Recoil to consolidate multiple changes into a single update cycle, significantly improving performance, especially when dealing with complex state transitions. **Example:** """javascript import { batch } from 'recoil'; import { useRecoilState } from 'recoil'; import { atom1State, atom2State, atom3State } from './atoms'; function MultiUpdateComponent() { const [atom1, setAtom1] = useRecoilState(atom1State); const [atom2, setAtom2] = useRecoilState(atom2State); const [atom3, setAtom3] = useRecoilState(atom3State); const updateAllAtoms = () => { batch(() => { setAtom1(atom1 + 1); setAtom2(atom2 + 2); setAtom3(atom3 + 3); }); }; return <button onClick={updateAllAtoms}>Update All</button>; } """ In this example, all three atoms are updated within a single update cycle, preventing the component from re-rendering multiple times. ### 3.5 "useRecoilCallback" * **Do This**: Use "useRecoilCallback" when an event handler or callback needs to read or write multiple Recoil states without causing intermediate renders of the component itself. * **Don't Do This**: Use "useRecoilState" and "useSetRecoilState" within the same event handler if only a final state update is desired; this may trigger extra renders. **Why**: "useRecoilCallback" provides a way to encapsulate complex state interactions without causing the component to re-render until all the updates are complete. **Example**: """javascript import { useRecoilCallback } from 'recoil'; import { todoListState, todoListFilterState } from './atoms'; function TodoListActions() { const addFilteredTodo = useRecoilCallback( ({ set, snapshot }) => async (text) => { const filter = await snapshot.getPromise(todoListFilterState); const newTodo = { id: Date.now(), text, isComplete: filter === 'Show Completed', }; set(todoListState, (prevTodoList) => [...prevTodoList, newTodo]); }, [] ); return ( <button onClick={() => addFilteredTodo('New Todo')}> Add Filtered Todo </button> ); } """ ## 4. Asynchronous Data Handling ### 4.1 Asynchronous Selectors * **Do This:** Utilize asynchronous selectors for fetching data from APIs or performing other asynchronous operations. * **Don't Do This:** Block the main thread with synchronous operations within selectors. **Why:** Asynchronous selectors allow you to fetch data without blocking the UI, improving responsiveness. **Example:** """javascript import { selector } from 'recoil'; export const userDataState = selector({ key: 'userDataState', get: async () => { const response = await fetch('/api/user'); const data = await response.json(); return data; }, }); """ ### 4.2 Handling Loading States * **Do This:** Implement "Suspense" for handling loading states in asynchronous selectors, providing a smooth user experience while data is being fetched. * **Don't Do This:** Display blank screens or error messages without providing informative feedback during loading. **Why:** "Suspense" allows you to display fallback content while waiting for asynchronous operations to complete, preventing jarring UI transitions. **Example:** """javascript import React, { Suspense } from 'react'; import { useRecoilValue } from 'recoil'; import { userDataState } from './selectors'; function UserProfile() { const user = useRecoilValue(userDataState); return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); } function App() { return ( <Suspense fallback={<div>Loading user data...</div>}> <UserProfile /> </Suspense> ); } """ ### 4.3 Error Handling * **Do This:** Implement proper error handling in asynchronous selectors to catch and handle potential exceptions. * **Don't Do This:** Ignore errors or allow them to propagate unhandled, which can lead to unexpected application behavior. **Why:** Robust error handling ensures that your application gracefully handles unexpected errors, preventing crashes and providing informative feedback to the user. Implement try/catch blocks within the selector, or utilize "useRecoilValueLoadable" to extract loading and error states alongside the data. **Example:** """javascript import { selector, useRecoilValueLoadable } from 'recoil'; export const asyncDataState = selector({ key: 'asyncDataSelector', get: async () => { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } catch (error) { console.error("Error fetching data:", error); throw error; // Re-throw the error to be caught in the component } } }); function MyComponent() { const { state, contents } = useRecoilValueLoadable(asyncDataState); switch (state) { case 'hasValue': return <div>Data: {contents.data}</div>; case 'loading': return <div>Loading...</div>; case 'hasError': return <div>Error: {contents.message}</div>; } } """ ## 5. DevTools Usage ### 5.1 Inspecting Atom Values * **Do This**: Regularly inspect the value of atoms in the Recoil DevTools to ensure that data is consistent with expected use cases. * **Don't Do This**: Treat atom values as "black boxes". Proactively use the tools to verify data integrity. **Why**: DevTools expose the current value and modification history of an atom. This makes it easy to debug issues related to unexpected state updates, or to track down the cause of a performance bottleneck. ### 5.2 Monitoring Selector Performance * **Do This**: Use the DevTools' profiling capabilities to identify selectors that are taking an unexpectedly long time to compute their derived state. * **Don't Do This**: Rely on guesswork to pinpoint performance problems. Measure and analyze the time it takes selectors to execute. **Why**: Slow selectors can be a major source of jank and lag in a Recoil app. The DevTools provide a breakdown of selector execution times, making it easy to identify and optimize slow code. ## 6. Avoiding Common Anti-Patterns ### 6.1 Over-Reliance on Global State * **Don't Do This:** Store all application state in Recoil atoms. Carefully consider whether state is truly global or can be managed locally within components. * **Do This:** Use Recoil primarily for state that is shared across multiple components or that needs to persist across route changes. Local state management can often be more performant for component-specific data. **Why:** Excessive use of global state can lead to unnecessary re-renders and make it harder to reason about application behavior. ### 6.2 Direct DOM Manipulation * **Don't Do This:** Directly manipulate the DOM in response to Recoil state changes. * **Do This:** Allow React to manage the DOM based on changes in the virtual DOM. **Why:** Recoil is designed to work in harmony with React's rendering model. Direct DOM manipulation bypasses React's optimizations and can lead to inconsistencies and performance problems. ### 6.3 Neglecting Unsubscribe * **Don't Do This:** Define subscriptions/observers that are not properly unsubscribed when a component unmounts. * **Do This:** Use "useEffect" with a cleanup function to handle subscription and unsubscription to ensure you don't have memory leaks and unexpected behaviour. **Why:** Failing to unsubscribe from Recoil atoms when a component unmounts can lead to memory leaks and unexpected behavior.