# Security Best Practices Standards for Recoil
This document outlines security best practices for developing applications using Recoil. Following these standards will help protect against common vulnerabilities, ensure data integrity, and maintain the confidentiality of your application's information.
## 1. Preventing Common Vulnerabilities in Recoil Applications
Recoil, like other front-end state management libraries, can be susceptible to common web application vulnerabilities if not properly implemented. It's crucial to understand how these vulnerabilities can manifest in a Recoil context and how to mitigate them.
### 1.1 Cross-Site Scripting (XSS)
XSS vulnerabilities allow attackers to inject malicious scripts into your application, potentially stealing user data or performing actions on their behalf. Recoil itself doesn't directly introduce XSS vulnerabilities, but improper handling of data fetched from external sources or user input can create attack vectors.
**Standard:** Always sanitize and escape user input and data fetched from untrusted sources before rendering it in your React components.
**Why:** Prevents malicious scripts from being executed in the user's browser.
**Do This:**
* Use React's built-in escaping mechanisms, like JSX, which automatically escapes values.
* Employ a library like DOMPurify when rendering HTML from potentially untrusted sources.
**Don't Do This:**
* Directly render user input or unsanitized external data using "dangerouslySetInnerHTML".
**Code Example (Safe Sanitization with DOMPurify):**
"""jsx
import React from 'react';
import DOMPurify from 'dompurify';
import { useRecoilValue } from 'recoil';
import { userInputState } from './atoms'; // Assuming you store user input in Recoil
function DisplayUserInput() {
const userInput = useRecoilValue(userInputState);
const sanitizedHTML = DOMPurify.sanitize(userInput);
return (
);
}
export default DisplayUserInput;
"""
**Explanation:** The "DOMPurify.sanitize()" function removes potentially malicious HTML from the user input before it's rendered. Storing the input in a Recoil atom doesn't inherently introduce risk, but proper sanitization is *essential* before presentation.
### 1.2 Cross-Site Request Forgery (CSRF)
CSRF attacks trick a user into performing actions on a website without their knowledge. While Recoil doesn't introduce CSRF vulnerabilities directly, it's important to be aware of the risk when handling state updates that trigger backend requests.
**Standard:** Implement CSRF protection for all state updates that result in server-side actions (e.g., submitting forms, updating data via API calls).
**Why:** Ensures that only legitimate user-initiated actions are processed by the server.
**Do This:**
* Use anti-CSRF tokens in your backend and include them in your requests.
* Set the "SameSite" attribute of your session cookie to "Strict" or "Lax" to prevent cross-site requests from including the cookie by default.
**Don't Do This:**
* Rely solely on client-side validation for security.
* Store sensitive data in the browser's local storage without proper encryption. Prefer cookies with the "HttpOnly" flag when possible.
**Code Example (Adding CSRF Token to API Request initiated by Recoil state change):**
"""jsx
import { atom, useRecoilCallback } from 'recoil';
import axios from 'axios';
const apiEndpointState = atom({
key: 'apiEndpointState',
default: '', // Start with an initial value
});
const updateData = useRecoilCallback(
({snapshot}) => async (data) => {
const apiEndpoint = await snapshot.getPromise(apiEndpointState);
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); // Assuming CSRF token is in meta tag
try {
const response = await axios.post(apiEndpoint, data, {
headers: {
'X-CSRF-Token': csrfToken,
},
});
return response.data;
} catch (error) {
// Handle errors appropriately
console.error("API request failed:", error);
throw error; // Re-throw to allow component error boundaries to catch it
}
}, []);
"""
**Explanation:** This example demonstrates how to fetch a CSRF token (assuming it's included in the HTML - a common practice) and include it in the headers of an API request triggered by a state change. "useRecoilCallback" is used for performing side effects based on other atoms' values, making it ideal for such scenarios. The "apiEndpointState" atom dynamically provides the API target based on a configuration, which can itself be dynamically updated. Error handling is crucial; the "Error" re-throw allows component error boundaries to manage the user experience after a failed API call.
### 1.3 Injection Attacks
Injection attacks, such as SQL injection, occur when untrusted data is sent to an interpreter, allowing the attacker to inject malicious code. While Recoil primarily manages front-end state, API calls initiated from Recoil-managed code can be vulnerable if the backend doesn't properly sanitize data.
**Standard:** Always validate and sanitize data on the backend before using it in queries or commands.
**Why:** Prevents malicious code from being executed on the server.
**Do This:**
* Use parameterized queries or prepared statements when interacting with databases.
* Implement strong input validation on the backend to ensure that data conforms to expected formats and lengths.
**Don't Do This:**
* Concatenate user input directly into SQL queries or shell commands.
**Code Example (Backend Data Validation triggered by Recoil State):**
(This example focuses on the client side, demonstrating state management and the *need* for server-side security)
"""jsx
import { atom, useSetRecoilState } from 'recoil';
const searchQueryState = atom({
key: 'searchQueryState',
default: '',
});
function SearchComponent() {
const setSearchQuery = useSetRecoilState(searchQueryState);
const handleInputChange = (event) => {
setSearchQuery(event.target.value);
};
return (
);
}
export default SearchComponent;
"""
**Backend (Example - Node.js with Express and Sequelize - demonstrating parameterization):**
"""javascript
// Server-side (Node.js with Express and Sequelize)
const express = require('express');
const { QueryTypes } = require('sequelize');
const { sequelize } = require('./models'); // Assuming you have a Sequelize instance
const app = express();
app.use(express.json());
app.post('/api/search', async (req, res) => {
const searchQuery = req.body.query;
// Sanitize the input (example using a simple regex)
const sanitizedQuery = searchQuery.replace(/[^a-zA-Z0-9\s]/g, ''); // Remove special characters for this example
// Use parameterized query to prevent SQL injection
try {
const results = await sequelize.query(
'SELECT * FROM products WHERE name LIKE :searchQuery',
{
replacements: { searchQuery: "%${sanitizedQuery}%" },
type: QueryTypes.SELECT
}
);
res.json(results);
} catch (error) {
console.error("Database error:", error);
res.status(500).json({ error: 'Failed to execute search' });
}
});
"""
**Explanation:** The React component captures the search query and updates the "searchQueryState" atom. While the client doesn't directly cause the vulnerability, the backend *must* sanitize and parameterize the query. The server-side example shows how to sanitize the input string using a regular expression (more comprehensive sanitization is usually needed), and crucially, uses a parameterized query to avoid SQL injection vulnerabilities. Sequelize's "replacements" feature is vital. This isolates the application from risks of unsanitized user input.
### 1.4 Denial of Service (DoS)
DoS attacks attempt to make a service unavailable to legitimate users. In Recoil, computationally expensive state derivations or uncontrolled updates can potentially lead to client-side DoS.
**Standard:** Avoid computationally expensive operations in state derivations and limit the frequency of state updates.
**Why:** Prevents the application from becoming unresponsive or crashing due to excessive resource consumption.
**Do This:**
* Use memoization techniques (e.g., "useMemo" or "useCallback" in React) to avoid redundant calculations.
* Implement rate limiting on API requests to prevent abuse.
* Consider using web workers for computationally intensive tasks to offload them from the main thread.
**Don't Do This:**
* Perform complex calculations or network requests directly within a derived atom.
* Update state excessively in a loop or without proper throttling.
**Code Example (Memoizing a Derived State):**
"""jsx
import { atom, selector, useRecoilValue } from 'recoil';
import { useMemo } from 'react';
const rawDataState = atom({
key: 'rawDataState',
default: [], // Initially an empty array
});
const processedDataState = selector({
key: 'processedDataState',
get: ({ get }) => {
const rawData = get(rawDataState);
// Imagine a complex transformation here.
const processedData = useMemo(() => {
console.log("Processing raw data...");
return rawData.map(item => ({
...item,
// complex transformation
transformedValue: item.value * 2
}));
}, [rawData]);
return processedData;
},
});
function DisplayProcessedData() {
const processedData = useRecoilValue(processedDataState);
return (
{processedData.map(item => (
{item.transformedValue}
))}
);
}
export default DisplayProcessedData;
"""
**Explanation:** The "processedDataState" is derived from "rawDataState". The key is to use "useMemo" *within the selector* to memoize the potentially expensive "rawData.map" operation. This ensures that the processing only occurs when "rawData" changes, preventing unnecessary re-calculations and potential performance issues that could lead to DoS-like behavior on the client's browser. The "console.log" clearly indicates when this operation happens. Initially empty, pushing data rapidly to the array without "useMemo" would lead to repeated expensive calculations if it involved many complex transforms, calculations, or sorts.
## 2. Secure Coding Patterns for Recoil
Implementing secure coding patterns specifically tailored to Recoil can significantly improve the overall security posture of your application.
### 2.1 Immutable State Updates
**Standard:** Treat Recoil state as immutable and always update it using the provided "set" function or updater functions.
**Why:** Avoids accidental state mutations that can lead to unexpected behavior and potential security vulnerabilities. Immutable data patterns make debugging and reasoning about application flow substantially easier, preventing errors where unintended mutation had security implications.
**Do This:**
* Create new copies of objects or arrays when updating state. Use the spread operator ("...") or methods like "map" and "filter".
**Don't Do This:**
* Directly modify state objects or arrays (e.g., "state.push(newValue)" or "state.property = newValue").
**Code Example (Immutable State Update):**
"""jsx
import { atom, useRecoilState } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [], // Start with an empty array
});
function AddItemComponent() {
const [items, setItems] = useRecoilState(itemsState);
const addItem = () => {
const newItem = { id: Date.now(), name: 'New Item' };
setItems([...items, newItem]); // Using spread operator for immutability
};
return (
Add Item
{items.map(item => (
{item.name}
))}
);
}
export default AddItemComponent;
"""
**Explanation:** When adding a new item, a *new* array is created using the spread operator ("...items"). This ensures that the original "items" array is not mutated directly. Instead, "setItems" is called with the new array, which triggers a safe and predictable state update.
### 2.2 Secure Data Storage
**Standard:** Avoid storing sensitive information directly in Recoil atoms, especially data that should not reside in the client or be visible in dev tools.
**Why:** Sensitive data stored in Recoil is potentially visible in the browser's developer tools or accessible through client-side code.
**Do This:**
* Store sensitive information (e.g., API keys, passwords) securely on the server-side.
* If sensitive data *must* be handled on the client, encrypt it before storing it in Recoil and decrypt it only when needed. Consider using libraries like "sjcl" or the Web Crypto API.
**Don't Do This:**
* Store API keys, passwords, or other sensitive information directly in Recoil atoms without encryption.
**Code Example (Client-Side Encryption):**
"""jsx
// Note: This is a simplified example and should be adapted for production use with robust key management.
import { atom, useRecoilState } from 'recoil';
import CryptoJS from 'crypto-js'; // npm install crypto-js
const encryptedDataState = atom({
key: 'encryptedDataState',
default: null,
});
const encryptionKey = "YOUR_SECRET_KEY"; // VERY IMPORTANT: Never hardcode keys in production! Store securely.
function DataComponent() {
const [encryptedData, setEncryptedData] = useRecoilState(encryptedDataState);
const storeData = (data) => {
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), encryptionKey).toString();
setEncryptedData(ciphertext);
};
const retrieveData = () => {
if (encryptedData) {
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, encryptionKey);
const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
return decryptedData;
} catch (error) {
console.error("Decryption failed:", error);
return null;
}
}
return null;
};
return (
storeData({ sensitiveValue: "This needs encryption" })}>Store Data
console.log("Retrieved:", retrieveData())}>Retrieve Data
);
}
"""
**Explanation:** This example uses "crypto-js" to encrypt and decrypt data before storing it in the "encryptedDataState" atom. **Important:** The "encryptionKey" *must* be stored securely and *never* hardcoded in production code. Use environment variables or a secure key management system. This example demonstrates client-side *at rest* encryption. The retrieved data is only decrypted when actively retrieved. *Be very careful when storing anything remotely sensitive on the client*. This example is for illustrative purposes; a better approach would involve storing sensitive data server-side.
### 2.3 Secure State Persistence
**Standard:** If you are persisting Recoil state to local storage or cookies, ensure that sensitive data is encrypted before being stored and implement appropriate access controls.
**Why:** Persisted state can be vulnerable to tampering or unauthorized access if not properly secured.
**Do This:**
* Encrypt sensitive data before persisting it.
* Use appropriate access controls to limit who can access the persisted data.
* Be mindful of the size limitations of local storage and cookies.
**Don't Do This:**
* Store sensitive data in plain text.
* Store excessive amounts of data in local storage or cookies.
**Code Example (Encrypting Before Local Storage):**
"""jsx
import { atom, useRecoilState } from 'recoil';
import { useEffect } from 'react';
import CryptoJS from 'crypto-js';
const sensitiveInfoState = atom({
key: 'sensitiveInfoState',
default: '',
});
const encryptionKey = "YOUR_SECRET_KEY"; // Never hardcode in production!
function SensitiveInfoComponent() {
const [sensitiveInfo, setSensitiveInfo] = useRecoilState(sensitiveInfoState);
useEffect(() => {
// Load from localStorage on mount
const storedEncryptedInfo = localStorage.getItem('sensitiveInfo');
if (storedEncryptedInfo) {
try {
const bytes = CryptoJS.AES.decrypt(storedEncryptedInfo, encryptionKey);
const decryptedInfo = bytes.toString(CryptoJS.enc.Utf8);
setSensitiveInfo(decryptedInfo);
} catch(e) {
console.error("Decryption error at load:", e);
}
}
}, [setSensitiveInfo]);// Only runs on mount and unmount.
useEffect(() => {
// Save to localStorage on sensitiveInfo change
if (sensitiveInfo) {
const encryptedInfo = CryptoJS.AES.encrypt(sensitiveInfo, encryptionKey).toString();
localStorage.setItem('sensitiveInfo', encryptedInfo);
} else {
localStorage.removeItem('sensitiveInfo'); // Clean up if the value is removed
}
}, [sensitiveInfo]);
const handleInputChange = (e) => {
setSensitiveInfo(e.target.value);
}
return (
<p>Value: {sensitiveInfo || '[empty]'}</p>
);
}
export default SensitiveInfoComponent;
"""
**Explanation:** This stores the sensitive information using local storage, encrypting it first and decrypting it on retrieval. Local storage is suitable for non-critical data, but this approach *should not be used for highly sensitive information*. If persistence of truly sensitive data is required, utilize backend secure storage accessible through authenticated API calls, instead.
### 2.4 Input Validation
**Standard:** Validate user input and data fetched from external sources before storing it in Recoil state, ensuring data integrity and preventing unexpected behavior.
**Why:** Invalid or malicious data can corrupt the application state and lead to security vulnerabilities.
**Do This:**
* Use validation libraries (e.g., "zod", "yup") to define schemas and validate data.
* Implement both client-side and server-side validation to ensure data integrity. Server-side validation is absolutely critical for security.
**Don't Do This:**
* Rely solely on client-side validation for security. Client-side validation provides a better *user experience*, but is easily bypassed by a malicious actor.
* Store unsanitized raw input.
**Code Example (Input Validation with Zod):**
"""jsx
import { atom, useRecoilState } from 'recoil';
import { z } from 'zod';
// Define a Zod schema for user input
const userInputSchema = z.string().min(3).max(50); // Must be between 3 and 50 characters
const validatedInputState = atom({
key: 'validatedInputState',
default: '',
});
function InputComponent() {
const [validatedInput, setValidatedInput] = useRecoilState(validatedInputState);
const handleInputChange = (event) => {
const input = event.target.value;
try {
// Validate the input against the schema
userInputSchema.parse(input);
// If validation succeeds, update the state
setValidatedInput(input);
} catch (error) {
// Handle validation errors
console.error("Validation error:", error.errors);
// Optionally display an error message to the user
}
};
return (
<p>Value: {validatedInput || '[invalid]'}</p>
);
}
export default InputComponent;
"""
**Explanation:** This example uses Zod to define a schema for user input, ensuring it is a string between 3 and 50 characters. If validation fails, an error message is logged to the console (and could be displayed to the User). Even with client-side validation, always validate and sanitize data on the server-side as well.
## 3. Monitoring and Auditing
**Standard:** Implement monitoring and auditing mechanisms to detect and respond to potential security incidents.
**Why:** Proactive monitoring and auditing can help identify and mitigate security threats before they cause significant damage.
**Do This:**
* Log important events, such as user authentication, authorization failures, and data modifications.
* Monitor application performance and resource usage for unusual patterns that may indicate a security attack.
* Regularly review audit logs and security reports.
**Don't Do This:**
* Store sensitive data in audit logs without proper protection.
* Ignore security alerts and reports.
**Code Example (Basic Logging - Client-Only, Should be augmented with Backend Logging):**
"""jsx
import { atom, useSetRecoilState } from 'recoil';
const logMessagesState = atom({
key: 'logMessagesState',
default: []
});
function LogComponent() {
const setLogMessages = useSetRecoilState(logMessagesState);
const logEvent = (message) => {
setLogMessages(prevState => [...prevState, {
timestamp: new Date().toISOString(),
message: message
}]);
console.log("[${new Date().toISOString()}] ${message}"); // Also log to console for debugging
}
return (
logEvent("User action performed")}>Log Action
logEvent("Authorization failed")}>Log Auth Failure
);
}
"""
**Explanation:** While rudimentary, this demonstrates a conceptual logging implementation. In production, integrate a comprehensive logging solution that sends logs to a central server for analysis and monitoring. Consider using tools like Sentry, LogRocket, or similar services. *Crucially, logging client-side activities is never a substitute for backend logging and monitoring.*
By adhering to these coding standards, you can significantly reduce the risk of security vulnerabilities in your Recoil applications, ensuring the confidentiality, integrity, and availability of your data and services. Remember that security is an ongoing process and requires continuous monitoring, adaptation, and improvement.
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.