# 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'
# API Integration Standards for Recoil This document outlines the coding standards for integrating APIs with Recoil, focusing on maintainability, performance, and security. It's designed to guide developers and inform AI coding assistants to produce high-quality Recoil code. ## 1. Architecture and Patterns ### 1.1. Separation of Concerns **Standard:** Decouple API interaction logic from Recoil state management and UI components. **Do This:** Implement a dedicated layer (e.g., services or repositories) responsible for API calls. This layer should transform API responses into a format suitable for Recoil atoms/selectors. **Don't Do This:** Make direct API calls within React components or Recoil atoms/selectors. **Why:** Separation of concerns improves code testability, reusability, and reduces the cognitive load. Changes to the API or data fetching logic won't necessitate modifications in React components or Recoil state. **Example:** """typescript // api/productService.ts import axios from 'axios'; const API_URL = 'https://api.example.com/products'; export const fetchProducts = async () => { try { const response = await axios.get(API_URL); return response.data; // Assuming response.data is an array of product objects } catch (error) { console.error('Error fetching products:', error); throw error; // Re-throw the error to be handled by the caller. } }; export const createProduct = async (productData: any) => { try { const response = await axios.post(API_URL, productData); return response.data; } catch (error) { console.error('Error creating product:', error); throw error; } }; """ """typescript // recoil/productState.ts import { atom, selector } from 'recoil'; import { fetchProducts } from '../api/productService'; export const productListState = atom({ key: 'productListState', default: [], }); export const productListSelector = selector({ key: 'productListSelector', get: async ({ get }) => { try { const products = await fetchProducts(); return products; } catch (error) { // Handle error appropriately, e.g., show an error message console.error("Failed to fetch products in selector:", error); return get(productListState); //Return previous state or default state. } }, }); """ ### 1.2. Asynchronous Selectors **Standard:** Use asynchronous selectors for API-driven data fetching in Recoil. **Do This:** Leverage "selector" with an "async" "get" function to handle API calls within a Recoil selector. **Don't Do This:** Rely solely on "useEffect" in components to fetch data and update atoms, particularly if other Recoil state depends on that data. **Why:** Asynchronous selectors automatically manage the loading state and memoize the result, preventing unnecessary re-fetches. They also handle dependencies between different pieces of state better than "useEffect". **Example:** """typescript // recoil/userState.ts import { atom, selector } from 'recoil'; const USER_API_URL = 'https://api.example.com/user'; interface User { id: number; name: string; email: string; } export const userIdState = atom({ key: 'userIdState', default: 1, // Example initial user ID }); export const userState = atom<User | null>({ key: 'userState', default: null, }) export const userSelector = selector<User>({ key: 'userSelector', get: async ({ get }) => { const userId = get(userIdState); const response = await fetch("${USER_API_URL}/${userId}"); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); }, }); """ ### 1.3. Error Handling **Standard:** Implement robust error handling within your API integration logic. **Do This:** Wrap API calls in "try...catch" blocks. Manage loading and error states within your Recoil selectors or components. Utilize dedicated error boundary components to catch and display errors gracefully. **Don't Do This:** Ignore errors or let them bubble up unhandled. Assume API calls will always succeed. **Why:** Proper error handling prevents silent failures and provides a better user experience. Clear error messages assist in efficiently debugging and resolving issues. **Example:** """typescript // recoil/dataState.ts import { atom, selector } from 'recoil'; interface Data { id: number; value: string; } export const dataIsLoadingState = atom({ key: 'dataIsLoadingState', default: false, }); export const dataErrorState = atom({ key: 'dataErrorState', default: null as Error | null, }); export const dataState = selector<Data | null>({ key: 'dataState', get: async ({ set }) => { set(dataIsLoadingState, true); set(dataErrorState, null); // Clear any previous errors try { const response = await fetch('https://api.example.com/data'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const data = await response.json(); set(dataIsLoadingState, false); return data; } catch (error: any) { set(dataIsLoadingState, false); set(dataErrorState, error); throw error; // Re-throw the error to be caught by an error boundary if needed } }, }); """ """typescript // ErrorBoundary.tsx import React, { Component, ErrorInfo, ReactNode } from 'react'; interface Props { children: ReactNode; } interface State { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; } class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false, error: null, errorInfo: null }; } static getDerivedStateFromError(error: Error) { // Update state so the next render will show the fallback UI. return { hasError: true, error: error, errorInfo: null }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { // You can also log the error to an error reporting service console.error("Caught error:", error, errorInfo); this.setState({error: error, errorInfo: errorInfo}) } render() { if (this.state.hasError) { // You can render any custom fallback UI return ( <div> <h2>Something went wrong.</h2> <p>{this.state.error?.message}</p> <details style={{ whiteSpace: 'pre-wrap' }}> {this.state.errorInfo?.componentStack} </details> </div> ); } return this.props.children; } } export default ErrorBoundary; """ """typescript // Usage in App.tsx import ErrorBoundary from './ErrorBoundary'; import SomeComponentThatFetchesData f function App() { return ( <ErrorBoundary> <SomeComponentThatFetchesData /> </ErrorBoundary> ); } """ ### 1.4. Optimistic Updates **Standard:** When appropriate, implement optimistic updates for improved perceived performance during create/update operations. **Do This:** Immediately update the Recoil state based on the user's input, *before* the API call completes. Revert the state if the API call fails. **Don't Do This:** Wait for the API call to complete before updating the UI. **Why:** Provides a snappier user experience. Optimistic updates eliminate perceived latency for common operations. Handle remote validation for unexpected results. **Example:** """typescript // recoil/todoListState.ts import { atom, selector, useRecoilState } from 'recoil'; interface Todo { id: string; text: string; isComplete: boolean; } export const todoListState = atom<Todo[]>({ key: 'todoListState', default: [], }); const API_URL = 'https://api.example.com/todos'; // Async selector to fetch todos from API export const todoListSelector = selector({ key: 'todoListSelector', get: async ({get}) => { const response = await fetch(API_URL); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); }, }); export const useAddTodo = () => { const [todos, setTodos] = useRecoilState(todoListState); const addTodoOptimistic = async (text: string) => { const newTodo: Todo = { id: Math.random().toString(36).substring(7), // Temporary ID text, isComplete: false, }; // Optimistic update setTodos([...todos, newTodo]); try { const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newTodo), }); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const serverTodo: Todo = await response.json(); // Replace temporary ID with server ID setTodos(todos.map(todo => todo.id === newTodo.id ? serverTodo : todo)); } catch (error) { console.error('Error creating todo:', error); // Revert optimistic update on error setTodos(todos.filter(todo => todo.id !== newTodo.id)); } }; return addTodoOptimistic; }; """ ## 2. Data Handling ### 2.1. Data Transformation **Standard:** Transform API responses into a consistent and type-safe format before storing them in Recoil atoms. **Do This:** Create dedicated functions or classes to map API responses to your application's data models. **Don't Do This:** Store raw API responses directly in Recoil atoms. **Why:** Improves code maintainability and reduces coupling with the specific structure of the API response. Type safety ensures consistent data throughout the application. Prevents breaking changes down the line. **Example:** """typescript // api/mappers.ts interface ApiResponse { id: number; product_name: string; price_usd: number; } interface Product { id: number; name: string; price: number; // Stored in cents to avoid floating point issues } export const mapApiResponseToProduct = (apiResponse: ApiResponse): Product => { return { id: apiResponse.id, name: apiResponse.product_name, price: Math.round(apiResponse.price_usd * 100) // Convert to cents }; }; export const mapProductToApiResponse = (product: Product): ApiResponse => { return { id: product.id, product_name: product.name, price_usd: product.price / 100, // Convert to USD }; }; """ """typescript // recoil/productState.ts import { atom, selector } from 'recoil'; import { fetchProducts } from '../api/productService'; import { mapApiResponseToProduct } from '../api/mappers'; export const productListState = atom<Product[]>({ key: 'productListState', default: [], }); //productList selector export const productListSelector = selector<Product[]>({ key: 'productListSelector', get: async () => { const apiResponse = await fetchProducts(); // Assuming fetchProducts returns an array of ApiResponse return apiResponse.map(mapApiResponseToProduct); }, }); """ ### 2.2. Data Validation **Standard:** Validate data received from APIs to ensure data integrity and prevent unexpected errors. **Do This:** Use libraries like Zod, Yup, or io-ts to define schemas and validate API responses before storing them in Recoil atoms. **Don't Do This:** Trust that API responses will always conform to the expected format. **Why:** Protects against malformed data, preventing crashes or unexpected behavior. Provides early detection of API changes or inconsistencies. **Example (using Zod):** """typescript // api/schemas.ts import { z } from 'zod'; export const ProductSchema = z.object({ id: z.number().positive(), product_name: z.string().min(1), price_usd: z.number().nonnegative(), }); export const ProductArraySchema = z.array(ProductSchema); """ """typescript // api/productService.ts import axios from 'axios'; import { ProductArraySchema } from './schemas'; const API_URL = 'https://api.example.com/products'; export const fetchProducts = async () => { try { const response = await axios.get(API_URL); const parsedData = ProductArraySchema.parse(response.data); return parsedData; } catch (error) { console.error('Error fetching products:', error); throw error; } }; """ ### 2.3. Data Normalization **Standard:** Normalize data received from APIs to reduce redundancy and improve data consistency. **Do This:** Structure your Recoil atoms as normalized stores, where entities are stored in separate collections keyed by their IDs. Use selectors to join and derive data as needed by the UI. **Don't Do This:** Store denormalized or deeply nested data directly in Recoil atoms. **Why:** Normalization makes updates more efficient and prevents data inconsistencies. Reduces the risk of accidentally modifying data in multiple places, thus improving performance. **Example:** """typescript // recoil/entities.ts import { atom } from 'recoil'; interface User { id: string; name: string; email: string; } export const usersState = atom<Record<string, User>>({ key: 'usersState', default: {}, // Store users keyed by their IDs }); interface Post { id: string; userId: string; title: string; body: string; } export const postsState = atom<Record<string, Post>>({ key: 'postsState', default: {}, // Store posts keyed by their IDs }); """ """typescript // recoil/selectors.ts import { selector } from 'recoil'; import { usersState, postsState } from './entities'; export const userPostsSelector = selector({ key: 'userPostsSelector', get: ({ get }) => { const users = get(usersState); const posts = get(postsState); // Implement logic to join users and posts based on userId return Object.values(posts).map((post) => ({ ...post, user: users[post.userId] })); }, }); """ ## 3. Network Requests ### 3.1. Request Deduplication **Standard:** Implement request deduplication to prevent sending redundant API requests. **Do This:** If the same selector is being read multiple times simultaneously, ensure that fetching is only triggered once. Recoil's memoization helps, but consider a dedicated deduplication layer for complex scenarios. **Don't Do This:** Allow multiple identical API requests to be triggered concurrently. **Why:** Reduces server load and improves application performance. Prevents race conditions and data inconsistencies. **Example:** Recoil's selectors provide built-in memoization. If multiple components read the same selector concurrently, the API request will only be dispatched once. However, for more complex scenarios, consider using a library like "axios-hooks", which has built-in request deduplication capabilities. """typescript import { useAxios } from 'axios-hooks' import { productListState } from './recoil/productState' import { useSetRecoilState } from 'recoil' const ProductList = () => { const setProducts = useSetRecoilState(productListState); const [{ data, loading, error }, refetch] = useAxios( 'https://api.example.com/products' ) useEffect(() => { if (data) { setProducts(data) } }, [data, setProducts]) } """ ### 3.2. Caching **Standard:** Implement caching strategies to reduce network requests and improve performance. **Do This:** Cache API responses using browser storage (e.g., localStorage, sessionStorage, IndexedDB) or a server-side caching layer (e.g., Redis, Memcached). Consider using the Recoil "atomFamily" to create individual atoms/selectors for each resource, allowing for more granular caching. Use "Recoilize" to persist recoil state easily. **Don't Do This:** Rely solely on browser caching without implementing proper cache invalidation strategies. **Why:** Significantly improves application load times and reduces server load. Provides a better user experience, especially for users with slow or unreliable network connections. **Example:** """typescript // recoil/cachedDataState.ts import { atom, selector } from 'recoil'; import { localStorageEffect } from 'recoiljs-toolkit'; const API_URL = 'https://api.example.com/data'; interface Data { id: number; value: string; } //Persist recoil state to local storage export const cachedDataState = atom<Data | null>({ key: 'cachedDataState', default: null, effects: [ localStorageEffect('cachedData'), ], }); export const fetchDataSelector = selector<Data>({ key: 'fetchDataSelector', get: async ({get}) => { const cachedData = get(cachedDataState); if(cachedData){ console.log("Data retrieved from cache"); return cachedData; } const response = await fetch(API_URL); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const data = await response.json(); return data; }, }); """ ### 3.3. Throttling and Debouncing **Standard:** Implement throttling or debouncing to limit the frequency of API calls triggered by user input. **Do This:** Use libraries like Lodash's "throttle" or "debounce" functions to control the rate at which API requests are sent. **Don't Do This:** Send API requests on every keystroke or mouse movement, especially for autosave functionality. **Why:** Reduces server load and improves application responsiveness. Prevents overwhelming the API with excessive requests. **Example:** """typescript import { atom, selector } from 'recoil'; import { debounce } from 'lodash'; export const searchTermState = atom({ key: 'searchTermState', default: '', }); export const debouncedSearchTermState = atom({ key: 'debouncedSearchTermState',; default: '', }); export const debouncedSearchEffect = ({setSelf, onSet}:any) => { onSet((newValue:string) => { const debounced = debounce((newValue:string) => { setSelf(newValue) }, 500); debounced(newValue) }) } export const searchTermSelector = selector({ key: 'searchTermSelector', get: ({ get }) => { const term = get(debouncedSearchTermState); // Call the API with the debounced search term }, }); """ ## 4. Security ### 4.1. Authentication and Authorization **Standard:** Implement proper authentication and authorization mechanisms to protect your API endpoints. **Do This:** Use secure authentication protocols like OAuth 2.0 or JWT (JSON Web Tokens). Store API keys and credentials securely (e.g., using environment variables or a secrets management system). Implement authorization checks on the server-side to ensure users only have access to the resources they are allowed to access. **Don't Do This:** Store API keys directly in client-side code or expose them in network requests. Rely solely on client-side authorization checks. **Why:** Protects sensitive data and prevents unauthorized access to your API. Ensures data integrity and compliance with security regulations. ### 4.2. Data Sanitization **Standard:** Sanitize any user-provided data before sending it to the API to prevent injection attacks. **Do This:** Use libraries like DOMPurify to sanitize HTML input. Escape user-provided data before including it in database queries or API requests. **Don't Do This:** Trust that user input is safe and does not contain malicious code. **Why:** Prevents security vulnerabilities like cross-site scripting (XSS) and SQL injection. Ensures that your application is protected against malicious attacks. ### 4.3. Rate Limiting **Standard:** Implement rate limiting on your API endpoints to prevent abuse and denial-of-service (DoS) attacks. **Do This:** Use middleware or server-side configuration to limit the number of requests that can be made from a single IP address or user account within a specific time period. **Don't Do This:** Allow unlimited requests to your API endpoints, which could be exploited by attackers. **Why:** Protects your API from abuse and ensures availability for legitimate users. Prevents attackers from overwhelming your server with malicious traffic. ## 5. Testing ### 5.1. Unit Tests **Standard:** Write unit tests for your API integration logic, including API service functions and data mappers. **Do This:** Mock API responses and verify that your code correctly transforms and processes the data. Test error handling scenarios to ensure that errors are handled gracefully. **Don't Do This:** Skip unit tests for API integration logic, which could lead to undetected errors and regressions. **Example:** """typescript // api/productService.test.ts import { fetchProducts } from './productService'; import axios from 'axios'; jest.mock('axios'); const mockedAxios = axios as jest.Mocked<typeof axios>; describe('productService', () => { it('should fetch products successfully', async () => { const mockProducts = [{ id: 1, name: 'Product 1', price: 10 }]; mockedAxios.get.mockResolvedValue({ data: mockProducts }); const products = await fetchProducts(); expect(products).toEqual(mockProducts); expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/products'); }); it('should handle errors gracefully', async () => { mockedAxios.get.mockRejectedValue(new Error('Failed to fetch')); await expect(fetchProducts()).rejects.toThrow('Failed to fetch'); }); }); """ ### 5.2. Integration Tests **Standard:** Write integration tests to verify the interaction between your React components, Recoil state, and API endpoints. **Do This:** Use testing libraries like React Testing Library or Cypress to simulate user interactions and verify that the UI updates correctly based on API responses. Mock API responses to ensure consistent test results. **Don't Do This:** Rely solely on unit tests, which may not catch integration issues between different parts of your application. ### 5.3. End-to-End Tests **Standard:** Write end-to-end tests to verify the entire application flow, from the user interface to the backend API. **Do This:** Use testing frameworks like Cypress or Playwright to automate browser interactions and verify that the application functions correctly from end to end. Test different user scenarios and edge cases to ensure application robustness. **Don't Do This:** Skip end-to-end tests, which could lead to undetected issues in the overall application flow. Write brittle tests that are prone to failure due to minor UI changes. By adhering to these coding standards, you can build robust, maintainable, and secure Recoil applications that effectively integrate with external APIs. Always refer to the official Recoil documentation for the latest features and best practices.
# Tooling and Ecosystem Standards for Recoil This document outlines the recommended tooling and ecosystem practices for Recoil development. Adhering to these standards ensures maintainable, performant, and secure applications. ## 1. Development Environment Setup ### 1.1. Editor and IDE Configuration **Standard:** Use a modern code editor or IDE with strong JavaScript/TypeScript support, ESLint integration, and Prettier formatting. Configure the editor to format and lint on save. **Why:** Consistent formatting and linting improve code readability and help catch errors early. **Do This:** * Use VS Code, WebStorm, or a comparable IDE. * Install and configure ESLint and Prettier. * Use EditorConfig for consistent code style across different editors. * Integrate a Recoil-specific extension if available (currently, there isn't a dedicated widely-used extension, but this may change). **Don't Do This:** * Rely on inconsistent manual formatting. * Ignore ESLint warnings and errors. **Example (.eslintrc.js):** """javascript module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint', 'prettier'], rules: { 'prettier/prettier': 'error', 'react/react-in-jsx-scope': 'off', //since React 17 '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': 'warn', // add more specific rules as needed }, settings: { react: { version: 'detect', }, }, }; """ ### 1.2. TypeScript Adoption **Standard:** Use TypeScript for all new Recoil projects. For existing JavaScript projects, incrementally migrate to TypeScript. **Why:** TypeScript provides static typing, improved code discoverability, and better IDE support, resulting in fewer runtime errors and easier refactoring. **Do This:** * Configure the "tsconfig.json" file with strict compiler options. * Define clear types for all Recoil atoms and selectors. * Use generics to create reusable, type-safe components that interact with Recoil state. **Don't Do This:** * Use "any" excessively; strive for specific types. * Disable strict compiler options. **Example (TypeScript Atom):** """typescript import { atom } from 'recoil'; interface User { id: string; name: string; email: string; } const userState = atom<User>({ key: 'userState', default: { id: '', name: '', email: '' }, }); export default userState; """ ## 2. State Management Debugging and Monitoring ### 2.1. Recoil DevTools **Standard:** Use the Recoil DevTools extension during development to inspect and debug Recoil state. **Why:** Recoil DevTools provide a visual representation of the atom graph, allowing you to track state changes, identify performance bottlenecks, and understand data flow. **Do This:** * Install the Recoil DevTools browser extension. * Ensure your application is properly connected. Verify you can see the updates in the devtools. * Use the DevTools to inspect atom values, selector computation timings, and data dependencies. **Don't Do This:** * Ignore performance warnings or errors reported by the DevTools. * Leave DevTools enabled in production builds. Conditionally disable it in production. **Example (Conditional DevTools Inclusion):** """jsx import { RecoilRoot } from 'recoil'; function App() { return ( <RecoilRoot debug={process.env.NODE_ENV === 'development'}> {/* Your application content */} </RecoilRoot> ); } export default App; """ ### 2.2. Logging and Analytics **Standard:** Integrate logging and analytics tools (e.g., Sentry, Firebase Analytics) to monitor application behavior and track errors in production. **Why:** Proactive monitoring helps identify and resolve issues quickly, improving user experience. Analyzing usage patterns can also help with future iterations. **Do This:** * Use error tracking services like Sentry to capture and report runtime exceptions. * Log critical state changes or user interactions to analytics platforms like Firebase Analytics or Amplitude. * Implement custom Recoil callbacks or selectors to track specific state transitions. **Don't Do This:** * Log sensitive user data (PII) without proper anonymization or encryption. * Over-log; focus on events that provide meaningful insights. **Example (Error Tracking with Sentry):** """javascript import * as Sentry from "@sentry/react"; import { BrowserTracing } from "@sentry/tracing"; Sentry.init({ dsn: "YOUR_SENTRY_DSN", integrations: [new BrowserTracing()], tracesSampleRate: 0.1, // Adjust in production }); function App() { return ( <Sentry.ErrorBoundary fallback={"An error has occured"}> {/* Your application content */} </Sentry.ErrorBoundary> ); } export default App; """ ## 3. Testing Strategies ### 3.1. Unit Testing **Standard:** Write unit tests for Recoil selectors and state transitions. **Why:** Unit tests ensure the correctness and predictability of Recoil logic. **Do This:** * Use testing frameworks like Jest or Mocha. * Mock external dependencies within selectors to isolate the unit under test. * Test different input values and edge cases. * Assert that state updates occur correctly. **Don't Do This:** * Skip unit tests for selectors with complex business logic. * Write brittle tests that are tightly coupled to implementation details. **Example (Jest Unit Test):** """javascript import { useRecoilValue } from 'recoil'; import { renderHook, act } from '@testing-library/react-hooks'; import { filteredTodoListState } from '../atoms'; // Assuming atoms.js import { todoListState } from '../atoms'; describe('filteredTodoListState', () => { it('should filter todo items based on the filter', async () => { const { result, waitForNextUpdate } = renderHook(() => useRecoilValue(filteredTodoListState), { wrapper: ({children}) => <RecoilRoot initializeState={({set}) => { set(todoListState, [{id: '1', text: 'Item1', isComplete: false}, {id: '2', text: 'Item2', isComplete: true}]); }}>{children}</RecoilRoot> }); await waitForNextUpdate(); expect(result.current.length).toBe(1); expect(result.current[0].id).toBe('2'); }); it('should return all items when filter is all', () => { const { result } = renderHook(() => useRecoilValue(filteredTodoListState), { wrapper: ({children}) => <RecoilRoot initializeState={({set}) => { set(todoListState, [{id: '1', text: 'Item1', isComplete: false}, {id: '2', text: 'Item2', isComplete: true}]); set(todoListFilterState, 'Show All') }}>{children}</RecoilRoot> }); expect(result.current.length).toBe(2); }); }); """ ### 3.2. Integration Testing **Standard:** Write integration tests to verify the interaction between components and Recoil state. **Why:** Integration tests ensure that components render correctly and dispatch state updates as expected. **Do This:** * Use testing libraries like React Testing Library or Enzyme. * Render components that consume Recoil state. * Simulate user interactions (e.g., button clicks, form submissions). * Assert that the UI updates correctly in response to state changes. **Don't Do This:** * Skip integration tests for components that heavily rely on Recoil state. * Over-mock; strive to test the actual interaction between components and Recoil. **Example (React Testing Library Integration Test):** """jsx import { render, screen, fireEvent, within } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; import TodoList from '../components/TodoList'; import { todoListState } from '../atoms'; // Adjust path function renderWithRecoil(ui, { initialState } = {}) { return render(<RecoilRoot initializeState={(snap) => { if (initialState) { for (const [atom, value] of Object.entries(initialState)) { snap.set(atom, value); } } }}>{ui}</RecoilRoot>); } describe('TodoList Component', () => { it('renders the todo list items correctly', () => { const initialState = { [todoListState]: [ { id: '1', text: 'Item 1', isComplete: false }, { id: '2', text: 'Item 2', isComplete: true }, ], }; renderWithRecoil(<TodoList />, { initialState }); const listItem1 = screen.getByText('Item 1'); const listItem2 = screen.getByText('Item 2'); expect(listItem1).toBeInTheDocument(); expect(listItem2).toBeInTheDocument(); }); it('can add a new todo item', async () => { renderWithRecoil(<TodoList />); const inputElement = screen.getByPlaceholderText('Add New Todo'); const addButton = screen.getByRole('button', { name: 'Add' }); fireEvent.change(inputElement, { target: { value: 'New Todo Item' } }); fireEvent.click(addButton); const newTodoItem = await screen.findByText('New Todo Item'); expect(newTodoItem).toBeInTheDocument(); }); }); """ ### 3.3 End-to-End (E2E) Testing **Standard:** Implement E2E tests to validate the complete user flow, including interactions with Recoil state in a realistic environment. **Why:** E2E tests catch issues that unit and integration tests might miss, ensuring end-to-end functionality. **Do This:** * Use E2E testing frameworks like Cypress or Playwright. * Simulate real user scenarios (e.g., logging in, filling out forms, navigating between pages). * Assert that state updates are reflected correctly across the entire application. * Use a testing environment that closely mirrors the production environment. **Don't Do This:** * Rely solely on manual testing. * Skip E2E tests for critical user flows. **Example (Cypress E2E Test - illustrative):** """javascript describe('Todo App E2E', () => { it('adds a new todo item', () => { cy.visit('/'); // Assuming your app is served at the root cy.get('input[placeholder="Add New Todo"]').type('Cypress Test Todo'); cy.get('button').contains('Add').click(); cy.contains('Cypress Test Todo').should('be.visible'); }); }); """ ## 4. Libraries and Tooling to Enhance Recoil ### 4.1. recoil-persist **Context:** Recoil itself doesn't handle persistence. "recoil-persist" is a popular library to persist and rehydrate Recoil state across sessions. **Standard:** Use "recoil-persist" to persist Recoil state in local storage, session storage, or other storage mechanisms. **Why:** Persistence ensures that user data is preserved even after the browser is closed or refreshed. **Do This:** * Install "recoil-persist". * Use the "persistAtom" effect to associate atoms with storage keys. * Configure the storage adapter (e.g., "localStorageAdapter", "sessionStorageAdapter"). * Handle potential storage errors gracefully. **Don't Do This:** * Store sensitive data in plain text in local storage; consider encryption. * Persist large or frequently updated atoms unnecessarily. **Example (Using recoil-persist):** """javascript import { atom } from 'recoil'; import { recoilPersist } from 'recoil-persist'; const { persistAtom } = recoilPersist(); const themeState = atom({ key: 'themeState', default: 'light', effects_UNSTABLE: [persistAtom], }); export default themeState; """ ### 4.2. recoil-sync **Context:** Recoil Sync facilitates synchronization of Recoil atoms between different browser tabs or windows. **Standard:** When needing shared state across multiple windows or tabs use "recoil-sync". **Why:** This ensures that all instances of your application display the same state. **Do This:** * Install "recoil-sync". * Configure the appropriate synchronization mechanism (e.g., BroadcastChannel, localStorage events). * Handle potential conflicts or race conditions. **Don't Do This:** * Over-synchronize Recoil state; synchronize only when necessary. Consider network bandwidth/performance cost. * Use "recoil-sync" for very large datasets, as it can impact performance. ### 4.3 Validation Libraries (Refine, Zod, Yup) **Standard**: When state comes from external or user-provided sources use a validation library to confirm the data meets expected requirements. **Why**: This prevents unexpected errors or vulnerabilities due to malformed data. **Do This**: * Select a validation library such as Refine (optimized for Recoil), Zod, or Yup. * Define schemas that describe the expected structure and types of your Recoil state. * Use these schemas to validate incoming data before updating atoms. **Don't Do This**: * Neglect validation and assume data is always correct. * Rely solely on client-side validation; always validate on the server as well. **Example (Using Refine):** """javascript import { atom } from 'recoil'; import { refineRecoil } from '@rocicorp/refine-recoil'; import { string, object } from 'refine'; const userSchema = object({ name: string(), email: string().email(), }); const userState = atom({ key: 'userState', default: { name: '', email: '' }, effects_UNSTABLE: [ refineRecoil(userSchema.validate), ], }); export default userState; """ ### 4.4 Testing the Recoil Ecosystem Parts **Standard**: Appropriately test any external libraries or integrations that are used to enhance Recoil. **Why**: To avoid relying on integrations that don't behave as expected. **Do This**: * For recoil-persist write tests to confirm data survives reload/session changes. * For recoil-sync confirm synchronization between browser contexts. * For validation libraries verify data is properly validated before state is updated. **Don't Do This**: * Assume that these integrations are flawless. Be cautious. ## 5. Performance Optimization ### 5.1. Selector Memoization **Standard:** Optimize selector performance by leveraging memoization. **Why:** Memoization prevents unnecessary recalculations when selector dependencies haven't changed, improving application responsiveness. **Do This:** * Let Recoil handle memoization automatically. Recoil has the concept built in. * Ensure selector dependencies are stable. * Use "selectorFamily" for parameterized selectors with memoization. **Don't Do This:** * Over-optimize; focus on selectors that are known to be performance bottlenecks. * Introduce unnecessary complexities in selector logic. ### 5.2. Atom Family Considerations **Standard:** Be judicious using Atom Families. **Why:** Atom Families can create many Atoms. **Do This:** * Use Atom Families only when truly needed. * Consider if a derived value can achieve the same effect without the performance overhead. **Don't Do This:** * Automatically use an Atom family. * Create Atoms that aren't properly garbage collected. ### 5.3. Bundle Size Minimization **Standard:** Reduce the bundle size by tree-shaking unused Recoil modules and dependencies. **Why:** Smaller bundle sizes result in faster load times and improved user experience. **Do This:** * Use a module bundler like Webpack or Parcel with tree-shaking enabled. * Import only the necessary Recoil modules: "import { atom, selector } from 'recoil';" instead of "import * as Recoil from 'recoil';". * Analyze bundle size with tools like Webpack Bundle Analyzer to identify large dependencies. **Don't Do This:** * Include unnecessary Recoil modules or external libraries. * Ignore bundle size warnings or errors. ## 6. Security Considerations ### 6.1. Input Validation **Standard:** Validate all input data before updating Recoil state. **Why:** Input validation prevents vulnerabilities like Cross-Site Scripting (XSS) and SQL injection. **Do This:** * Use validation libraries like Zod or Yup to define schemas for input data. * Sanitize user-provided strings to remove potentially harmful characters. * Validate server responses before updating Recoil state. **Don't Do This:** * Trust user input implicitly. * Rely solely on client-side validation; validate on the server as well. ### 6.2. State Protection **Standard:** Limit direct access to Recoil atoms. Control state updates through selectors or custom hooks. **Why:** Protected state ensures data integrity and prevents accidental or malicious modifications. **Do This:** * Use read-only selectors to expose derived state to components. * Create custom hooks that encapsulate state update logic. * Avoid directly setting atom values from within components. **Don't Do This:** * Expose mutable atom values to untrusted components. * Allow arbitrary state updates from external sources. ### 6.3. Secure Storage **Standard:** Store sensitive data securely in encrypted storage mechanisms. **Why:** Secure storage prevents unauthorized access to confidential information. **Do This:** * Use libraries like "react-native-encrypted-storage" or browser's "crypto" API to encrypt sensitive data. * Store encryption keys securely (e.g., using environment variables or hardware-backed key storage). * Consider using secure HTTP headers to protect data in transit. **Don't Do This:** * Store sensitive data in plain text in local storage or cookies. * Expose encryption keys in client-side code.
# 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.
# 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.
# 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.