# 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 (
Name: setName(e.target.value)} />
Age: setAge(Number(e.target.value))} />
);
}
"""
### 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 {formattedAge};
}
"""
**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 Name: {name};
}
"""
**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 Loading...;
}
if (user.error) {
return Error: {user.error};
}
return (
{user.name}
<p>Email: {user.email}</p>
);
}
"""
**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 (
updateUser('Jane Doe', 31)}>Update User
);
}
"""
**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 (
Item {itemId}: setItem(e.target.value)} />
);
}
"""
**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 ;
};
};
// Use the HOC:
function MyComponent({ recoilValue }) {
return State Value: {recoilValue};
}
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 (
setText(e.target.value)}
/>
);
}
function List() {
const [items, setItems] = useState([]);
const add = () => {
setItems([...items, items.length]);
}
return (
Add
{items.map((id) => (
))}
);
}
"""
**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
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.
# 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.
# Testing Methodologies Standards for Recoil This document outlines the recommended testing methodologies for Recoil applications, covering unit, integration, and end-to-end testing strategies. It aims to provide clear guidance on how to effectively test Recoil code, ensuring maintainability, performance, and reliability. ## 1. General Testing Principles ### Do This * **Prioritize testing:** Write tests alongside your Recoil code, ideally adopting a test-driven development (TDD) approach. * **Test specific behavior:** Each test should target a specific, well-defined aspect of your code's behavior. * **Use descriptive test names:** Names should clearly describe what the test is verifying. Example: "should update atom value when action is dispatched". * **Keep tests independent:** Tests should not rely on each other's execution order or state. Each test should set up its own environment. * **Strive for high code coverage:** Aim for at least 80% code coverage, focusing on the most critical parts of your application. Use a coverage tool to assess this. * **Use mocks and stubs effectively:** Isolate units of code by mocking dependencies, particularly external APIs or complex components. ### Don't Do This * **Write tests as an afterthought:** Postponing testing often leads to neglected or poorly written tests. * **Test implementation details:** Tests should focus on behavior, not implementation. Changes to implementation details should not break your tests, if the behaviour remains the same. * **Use vague test names:** Avoid names like "testAtom" or "testComponent". * **Create tests that depend on state from other tests:** This leads to flaky and unreliable tests. * **Ignore code coverage:** Low code coverage means higher risk of undetected bugs. * **Over-mock or under-mock:** Over-mocking can lead to tests that are not realistic. Under-mocking can make tests slow and brittle. ### Why These Standards Matter * **Maintainability:** Well-tested code is easier to refactor and maintain. Tests act as a safety net, preventing regressions when changes are made. * **Performance:** Testing helps identify performance bottlenecks early in the development cycle. * **Reliability:** Thorough testing ensures that your application functions correctly and reliably under various conditions. ## 2. Unit Testing Recoil ### Do This * **Test individual atoms and selectors:** Verify that atoms and selectors update and derive values as expected under different conditions. * **Use "useRecoilValue" and "useRecoilState" hooks:** Access atom and selector values within your React components for testing purposes. Use testing libraries like "react-hooks-testing-library" or "@testing-library/react-hooks" to test components using these hooks. These libraries are designed to work in a React testing environment, running the hooks correctly and safely. * **Mock dependencies:** When testing selectors that depend on external data sources, mock the API calls. * **Use asynchronous testing for async selectors:** Properly handle asynchronous operations with "async/await" or promise-based testing. * **Isolate Recoil state:** Ensure each test has its own isolated Recoil state to avoid interference between tests. Use "RecoilRoot" inside each test setup. ### Don't Do This * **Directly modify atom values outside of a React component (in a test):** This breaks the Recoil model and can lead to unexpected side effects. Always use the proper hook methods for updates and reads. * **Ignore asynchronous behavior:** Failing to handle asynchronous operations in tests can lead to false positives or flaky tests. * **Skip state isolation:** Sharing Recoil state between tests leads to unpredictable and difficult-to-debug results. ### Code Examples #### Testing an Atom """javascript import { useRecoilState, RecoilRoot } from 'recoil'; import { atom } from 'recoil'; import { renderHook, act } from '@testing-library/react-hooks'; const countState = atom({ key: 'countState', default: 0, }); describe('countState Atom', () => { it('should initialize with a default value of 0', () => { const wrapper = ({ children }) => ( <RecoilRoot>{children}</RecoilRoot> ); const { result } = renderHook(() => useRecoilState(countState), { wrapper }); expect(result.current[0]).toBe(0); }); it('should update the atom value using the setter function', () => { const wrapper = ({ children }) => ( <RecoilRoot>{children}</RecoilRoot> ); const { result } = renderHook(() => useRecoilState(countState), { wrapper }); act(() => { result.current[1](1); // Update the atom value to 1 }); expect(result.current[0]).toBe(1); }); }); """ #### Testing a Selector """javascript import { useRecoilValue, RecoilRoot } from 'recoil'; import { atom, selector } from 'recoil'; import { renderHook } from '@testing-library/react-hooks'; const nameState = atom({ key: 'nameState', default: 'John', }); const greetingSelector = selector({ key: 'greetingSelector', get: ({ get }) => { const name = get(nameState); return "Hello, ${name}!"; }, }); describe('greetingSelector Selector', () => { it('should derive the correct greeting based on the nameState', () => { const wrapper = ({ children }) => ( <RecoilRoot>{children}</RecoilRoot> ); const { result } = renderHook(() => useRecoilValue(greetingSelector), { wrapper }); expect(result.current).toBe('Hello, John!'); }); it('should update the greeting when the nameState changes', () => { const wrapper = ({ children }) => ( <RecoilRoot initializeState={(snap) => snap.set(nameState, 'Jane')}> {children} </RecoilRoot> ); const { result } = renderHook(() => useRecoilValue(greetingSelector), { wrapper }); expect(result.current).toBe('Hello, Jane!'); }); }); """ #### Testing an Async Selector with Mocking """javascript import { useRecoilValue, RecoilRoot } from 'recoil'; import { atom, selector } from 'recoil'; import { renderHook } from '@testing-library/react-hooks'; // Mock the API call const mockFetchUserData = jest.fn(); const userIdState = atom({ key: 'userIdState', default: 1, }); const userDataSelector = selector({ key: 'userDataSelector', get: async ({ get }) => { const userId = get(userIdState); const response = await mockFetchUserData(userId); return response; }, }); describe('userDataSelector Selector with Mocking', () => { beforeEach(() => { mockFetchUserData.mockClear(); }); it('should fetch and return user data', async () => { mockFetchUserData.mockResolvedValue({ id: 1, name: 'John Doe' }); const wrapper = ({ children }) => ( <RecoilRoot>{children}</RecoilRoot> ); const { result, waitForNextUpdate } = renderHook(() => useRecoilValue(userDataSelector), { wrapper }); await waitForNextUpdate(); expect(result.current).toEqual({ id: 1, name: 'John Doe' }); expect(mockFetchUserData).toHaveBeenCalledWith(1); }); it('should handle errors during data fetching', async () => { mockFetchUserData.mockRejectedValue(new Error('Failed to fetch user data')); const wrapper = ({ children }) => ( <RecoilRoot>{children}</RecoilRoot> ); const { result, waitForNextUpdate } = renderHook(() => useRecoilValue(userDataSelector), { wrapper }); try { await waitForNextUpdate(); } catch(e){ expect(e.message).toEqual('Failed to fetch user data'); } expect(mockFetchUserData).toHaveBeenCalledWith(1); }); }); """ #### Testing "initializeState" The "initializeState" prop of "<RecoilRoot>" is an alternative way to initialize Recoil atoms for testing. """javascript import { RecoilRoot, atom, useRecoilValue } from 'recoil'; import { renderHook } from '@testing-library/react-hooks'; const myAtom = atom({ key: 'myAtom', default: 'initial value', }); it('should initialize the atom with a specific value', () => { const wrapper = ({ children }) => ( <RecoilRoot initializeState={(snap) => { snap.set(myAtom, 'test value'); }}> {children} </RecoilRoot> ); const { result } = renderHook(() => useRecoilValue(myAtom), { wrapper }); expect(result.current).toBe('test value'); }); it('should run initializeState only once per root', () => { let initializeStateCounter = 0; const wrapper = ({ children }) => ( <RecoilRoot initializeState={(snap) => { snap.set(myAtom, "value ${++initializeStateCounter}"); }}> {children} </RecoilRoot> ); const { result, rerender } = renderHook(() => useRecoilValue(myAtom), { wrapper }); expect(result.current).toBe('value 1'); expect(initializeStateCounter).toBe(1); rerender(); // Should not cause initializeState to re-run expect(result.current).toBe('value 1'); expect(initializeStateCounter).toBe(1); }); """ ## 3. Integration Testing Recoil ### Do This * **Test interactions between Recoil atoms, selectors, and React components:** Verify that changes to atoms correctly propagate through the application. * **Test components that use multiple Recoil hooks:** Ensure that these components handle state updates and re-renders correctly. * **Simulate user interactions:** Use libraries like "@testing-library/react" to simulate clicks, form submissions, and other user actions. * **Use a realistic test environment:** Configure your tests to closely resemble your production environment. * **Mock external APIs:** Use tools like "jest.mock" or "msw" (Mock Service Worker) to mock external APIs in integration tests. "msw" is preferred as it mocks at network level rather than at import level, making tests more maintainable and mimicking the browser closer. ### Don't Do This * **Test individual units in isolation:** Integration tests should focus on how different parts of the application work together. * **Rely on end-to-end tests for all integration concerns:** Integration tests should cover a wider range of scenarios more quickly than end-to-end tests. * **Hardcode data or assumptions:** Ensure your tests are resilient to changes in data or configuration. * **Avoid external API mocking:** Integration tests should not depend on the availability or stability of real external APIs. ### Code Examples #### Testing Component Interaction with Recoil State """javascript import React from 'react'; import { RecoilRoot, useRecoilState } from 'recoil'; import { atom } from 'recoil'; import { render, screen, fireEvent } from '@testing-library/react'; const textState = atom({ key: 'textState', default: '', }); function InputComponent() { const [text, setText] = useRecoilState(textState); const handleChange = (event) => { setText(event.target.value); }; return <input value={text} onChange={handleChange} />; } function DisplayComponent() { const [text, setText] = useRecoilState(textState); return <div>Current text: {text}</div>; } function App() { return ( <div> <InputComponent /> <DisplayComponent /> </div> ); } describe('Integration Testing: Input and Display Components', () => { it('should update the DisplayComponent when the InputComponent value changes', () => { render( <RecoilRoot> <App /> </RecoilRoot> ); const inputElement = screen.getByRole('textbox'); const displayElement = screen.getByText('Current text: '); fireEvent.change(inputElement, { target: { value: 'Hello, world' } }); expect(displayElement).toHaveTextContent('Current text: Hello, world'); }); }); """ #### Mocking External API in Integration Test using "msw" """javascript // src/mocks/handlers.js import { rest } from 'msw' export const handlers = [ rest.get('/api/todos', (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { id: 1, text: 'Write tests' }, { id: 2, text: 'Deploy code' }, ]) ) }), ] """ """javascript // src/setupTests.js import { server } from './mocks/server' // Establish API mocking before all tests. beforeAll(() => server.listen()) // Reset any request handlers that we may add during the tests, // so they don't affect other tests. afterEach(() => server.resetHandlers()) // Clean up after the tests are finished. afterAll(() => server.close()) """ """javascript // TodoList.test.js import React from 'react'; import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; import { atom, selector } from 'recoil'; import { render, screen, waitFor } from '@testing-library/react'; import { server } from './mocks/server'; import { rest } from 'msw'; const todoListState = atom({ key: 'todoListState', default: [], }); const fetchTodos = async () => { const response = await fetch('/api/todos'); return response.json(); }; const todoListSelector = selector({ key: 'todoListSelector', get: async () => { return await fetchTodos(); }, }); function TodoList() { const todos = useRecoilValue(todoListSelector); return ( <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } describe('TodoList Component - Integration with Mocked API', () => { it('should fetch and display todo items from the API', async () => { render( <RecoilRoot> <TodoList /> </RecoilRoot> ); await waitFor(() => { expect(screen.getByText('Write tests')).toBeInTheDocument(); expect(screen.getByText('Deploy code')).toBeInTheDocument(); }); }); it('should display an error message when the API call fails', async () => { server.use( rest.get('/api/todos', (req, res, ctx) => { return res(ctx.status(500), ctx.json({ message: 'Internal Server Error' })); }) ); render( <RecoilRoot> <TodoList /> </RecoilRoot> ); await waitFor(() => { const errorMessage = screen.queryByText('Internal Server Error'); // Or use another appropriate selector expect(errorMessage).toBeNull(); // Consider how errors are handled in your component. You might assert that an error boundary is now displayed. }); }); }); """ ## 4. End-to-End (E2E) Testing Recoil ### Do This * **Use E2E testing frameworks:** Cypress or Playwright. * **Simulate real user flows:** Test the entire application workflow from start to finish, including interactions with external systems. * **Test across multiple browsers and devices:** Ensure consistent behavior across different platforms. * **Use realistic data:** Populate your test database with data that resembles production data. * **Verify key metrics:** Ensure the proper operation of application by verifying functionality directly as seen by real users. ### Don't Do This * **Rely solely on E2E tests:** They are slow and can be difficult to maintain; supplement them with unit and integration tests. * **Test minor UI details:** Focus on critical functionality and user flows. * **Use hardcoded waits:** Use explicit waits or assertions to synchronise with asynchronous operations. * **Store secrets in the codebase:** Make sure keys and passwords are kept in environment variables. ### Code Examples #### Cypress Example Testing Recoil """javascript // cypress/integration/todo.spec.js describe('Todo App E2E Test', () => { beforeEach(() => { cy.visit('/'); // Replace with your app URL }); it('should add a new todo item and verify it is displayed', () => { const todoText = 'Learn Recoil'; cy.get('[data-testid="new-todo-input"]').type(todoText); cy.get('[data-testid="add-todo-button"]').click(); cy.get('[data-testid="todo-list"]').should('contain', todoText); }); it('should mark a todo item as complete', () => { cy.contains('Learn Recoil').parent().find('[data-testid="complete-todo-checkbox"]').check(); cy.contains('Learn Recoil').parent().should('have.class', 'completed'); }); it('should delete a todo item', () => { cy.contains('Learn Recoil').parent().find('[data-testid="delete-todo-button"]').click(); cy.get('[data-testid="todo-list"]').should('not.contain', 'Learn Recoil'); }); }); """ #### Playwright Example Testing Recoil """javascript // tests/todo.spec.ts import { test, expect } from '@playwright/test'; test('should add a new todo item', async ({ page }) => { await page.goto('/'); // Replace with your app URL const todoText = 'Learn Recoil'; await page.locator('[data-testid="new-todo-input"]').fill(todoText); await page.locator('[data-testid="add-todo-button"]').click(); await expect(page.locator('[data-testid="todo-list"]')).toContainText(todoText); }); test('should mark a todo item as complete', async ({ page }) => { await page.goto('/'); // Replace with your app URL await page.locator('[data-testid="complete-todo-checkbox"]').check(); await expect(page.locator('[data-testid="todo-list"] li').first()).toHaveClass('completed'); }); test('should delete a todo item', async ({ page }) => { await page.goto('/'); // Replace with your app URL await page.locator('[data-testid="delete-todo-button"]').click(); await expect(page.locator('[data-testid="todo-list"]')).not.toContainText('Learn Recoil'); }); """ ## 5. Common Anti-Patterns and Mistakes * **Ignoring Test Failures:** Treat test failures as critical issues that need immediate attention. * **Writing Flaky Tests:** Address flakiness by improving test isolation, using retry mechanisms, or addressing the underlying code issue. Favor deterministic tests. * **Over-Reliance on Mocks:** Limit the use of mocks and stubs to external dependencies and focus on testing real behavior. * **Neglecting Edge Cases:** Ensure tests cover edge cases, error conditions, and boundary values. * **Testing ImplementationDetails:** Your tests should not break if you refactor internals. * **Skipping performance testing:** Ensure to keep performance in mind. * **Not using test coverage tools:** Use a coverage tool to ensure every line and branch is covered. By adhering to these testing methodologies, you can ensure the quality, reliability, and maintainability of your Recoil applications. Remember to adapt these guidelines to your specific project requirements and context.