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