# Performance Optimization Standards for Jotai
This document outlines coding standards focused specifically on performance optimization when developing applications using Jotai. These standards are designed to improve application speed, responsiveness, and efficient resource utilization. We will cover practical techniques, address common anti-patterns, and provide clear "Do This" and "Don't Do This" guidance with illustrative code examples using the latest Jotai features.
## 1. Atom Definition and Scope
### 1.1. Minimize Atom Creation
**Do This:** Reuse atoms whenever logically possible, instead of creating new atoms for similar data.
**Don't Do This:** Create excessive atoms, especially within frequently rendered components, leading to unnecessary subscriptions and re-renders.
**Why:** Each atom you create in Jotai adds to the subscription overhead. Reusing atoms reduces memory consumption and the number of listeners to update.
"""javascript
// Do This: Reuse an atom
import { atom, useAtom } from 'jotai';
const counterAtom = atom(0);
function CounterComponent() {
const [count, setCount] = useAtom(counterAtom);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
// Don't Do This: create a NEW atom on every render
function BadCounterComponent() {
const [count, setCount] = useAtom(atom(0)); // Creates a new atom EVERY render
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
"""
### 1.2. Atom Scope and Provider Usage
**Do This:** Strategically scope atoms using "" components to manage state isolation and prevent unintended side effects. Only use Providers if needed. Default scope is global.
**Don't Do This:** Over-scope atoms globally when component-specific state is sufficient or under-scope resulting in data conflicts or missed updates.
**Why:** Careful scoping controls the atom's lifecycle and visibility, reducing unnecessary updates and simplifying debugging. Over-scoping pollutes the global state, leading to unpredictable behavior.
"""javascript
// Do This: Create a scope and use it.
import { atom, useAtom, Provider } from 'jotai';
import React from 'react';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
function App() {
return (
{/* Create a new scope */}
{/* Another Scope */}
);
}
// Don't Do This: Implicitly/Incorrectly rely on the global scope. Causes unexpected scope collissions.
const globalCountAtom = atom(0);
function BadCounter() {
const [count, setCount] = useAtom(globalCountAtom);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
function BadApp() {
return (
{/* This counter now manipulates the SAME global atom, causing unexpected behavior */}
);
}
"""
### 1.3. Derived Atoms and Selectors
**Do This:** Use derived atoms with "selectAtom" or "atom" with "get" to compute values based on other atoms *lazily*. Memoize complex computations within derived atoms to prevent unnecessary recalculations.
**Don't Do This:** Perform expensive calculations directly within components or in setters without memoization. Create unnecessary intermediate atoms.
**Why:** Derived atoms only recompute when their dependencies change. Memoization further optimizes this by caching the result of expensive calculations.
"""javascript
// Do This: Memoize expensive calculation
import { atom, useAtom } from 'jotai';
import { useMemo } from 'react';
const rawDataAtom = atom([1, 2, 3, 4, 5]);
const processedDataAtom = atom((get) => {
const data = get(rawDataAtom);
// Simulate an expensive operation
const processed = useMemo(() => { // useMemo to cache.
console.log('Processing data...'); // only runs if "data" changes!
return data.map(x => x * 2);
}, [data]);
return processed;
});
function DataComponent() {
const [processedData] = useAtom(processedDataAtom);
return (
<p>Processed Data: {processedData.join(', ')}</p>
);
}
// Don't Do This: Recalculate on EVERY render. The useMemo above is critical.
const badProcessedDataAtom = atom((get) => {
const data = get(rawDataAtom);
console.log('Processing data EVERY render...'); // runs always :(
return data.map(x => x * 2);
});
function DataComponent() {
const [processedData] = useAtom(badProcessedDataAtom);
return (
<p>Processed Data: {processedData.join(', ')}</p>
);
}
"""
### 1.4. Immutability
**Do This:** Always update atom values immutably. Use techniques such as the spread operator or "immer" to create new objects/arrays instead of modifying existing ones.
**Don't Do This:** Directly mutate the value stored within an atom.
**Why:** Jotai relies on change detection to trigger updates. Mutable updates bypass this mechanism, leading to inconsistent state and rendering issues.
"""javascript
// Do This: Use the spread operator for immutable updates
import { atom, useAtom } from 'jotai';
const itemsAtom = atom([{ id: 1, name: 'Apple' }]);
function ItemListComponent() {
const [items, setItems] = useAtom(itemsAtom);
const addItem = () => {
setItems([...items, { id: items.length + 1, name: 'Banana' }]);
};
return (
{items.map(item => (
{item.name}
))}
Add Item
);
}
// Do this: Use immer
import { useImmerAtom } from 'jotai/immer'
const itemsAtom = atom([{ id: 1, name: 'Apple' }]);
function ItemListComponent() {
const [items, update] = useImmerAtom(itemsAtom);
const addItem = () => {
update(draft => {
draft.push({ id: items.length + 1, name: 'Banana' })
})
};
return (
{items.map(item => (
{item.name}
))}
Add Item
);
}
// Don't Do This: Mutate the array directly!
function BadItemListComponent() {
const [items, setItems] = useAtom(itemsAtom);
const addItem = () => {
items.push({ id: items.length + 1, name: 'Banana' }); // Direct mutation!
setItems(items); // This MIGHT not trigger a re-render correctly even though it's updating the state
};
return (
{items.map(item => (
{item.name}
))}
Add Item
);
}
"""
## 2. Component Rendering and Subscriptions
### 2.1. Selective Atom Usage
**Do This:** Only subscribe to the atoms whose values are actually used within a component.
**Don't Do This:** Subscribe to atoms that a component doesn't directly need, leading to unnecessary re-renders.
**Why:** Minimizing subscriptions reduces the number of components that re-render when an atom changes.
"""javascript
// Do This: Use only necessary atom values
import { atom, useAtom } from 'jotai';
const userAtom = atom({ id: 1, name: 'John Doe', email: 'john.doe@example.com' });
function UserNameComponent() {
// Only subscribe to the 'name' property
const [userName] = useAtom(atom(get => get(userAtom).name));
return <p>User Name: {userName}</p>;
}
// Don't Do This: Subscribe to the entire atom when only name is needed
function BadUserNameComponent() {
const [user] = useAtom(userAtom);
return <p>User Name: {user.name}</p>; // Re-renders if ANY property of user changes
}
"""
### 2.2. Batched Updates
**Do This:** Utilize "batch" to group multiple atom updates into a single re-render cycle.
**Don't Do This:** Trigger multiple atom updates without batching, leading to excessive re-renders.
**Why:** Batching significantly reduces the number of re-renders, especially when performing related state updates.
"""javascript
//Do This: Use explicit batching
import { atom, useAtom, batch } from 'jotai';
const atomA = atom(0);
const atomB = atom(0);
function BatchedUpdateComponent() {
const [a, setA] = useAtom(atomA);
const [b, setB] = useAtom(atomB);
const updateBoth = () => {
batch(() => {
setA(a + 1);
setB(b + 1);
});
};
return (
<p>A: {a}, B: {b}</p>
Update Both
);
}
// Don't Do This: Update both without batching, leading to two re-renders!
function BadBatchedUpdateComponent() {
const [a, setA] = useAtom(atomA);
const [b, setB] = useAtom(atomB);
const updateBoth = () => {
setA(a + 1);
setB(b + 1);
};
return (
<p>A: {a}, B: {b}</p>
Update Both
);
}
"""
### 2.3. Debouncing and Throttling Updates
**Do This:** Debounce or throttle updates to atoms that are frequently updated, such as values from input fields. Use libraries like "lodash" or implement custom logic.
**Don't Do This:** Immediately update atoms on every input change, especially for expensive operations.
**Why:** Debouncing and throttling limit the frequency of updates, preventing performance bottlenecks caused by rapid state changes.
"""javascript
// Do This: Debounce input updates
import { atom, useAtom } from 'jotai';
import { useDebounce } from 'use-debounce';
const searchInputAtom = atom('');
function SearchInputComponent() {
const [searchInput, setSearchInput] = useAtom(searchInputAtom);
const [debouncedSearchInput] = useDebounce(searchInput, 500); // Debounce for 500ms
// Simulate a search function (expensive operation)
React.useEffect(() => {
console.log('Performing search for:', debouncedSearchInput);
// Replace with actual search logic
}, [debouncedSearchInput]);
return (
setSearchInput(e.target.value)}
/>
);
}
// Don't Do This: Perform the search on every key stroke.
function BadSearchInputComponent() {
const [searchInput, setSearchInput] = useAtom(searchInputAtom);
React.useEffect(() => {
console.log('Performing search for:', searchInput); // This is called a LOT.
}, [searchInput]);
return (
setSearchInput(e.target.value)}
/>
);
}
"""
## 3. Asynchronous Operations
### 3.1. Atom-Based Caching
**Do This:** Use atoms to cache results from asynchronous operations to avoid redundant API calls. Implement appropriate cache invalidation strategies.
**Don't Do This:** Repeatedly fetch data from APIs without caching, especially within frequently rendered components.
**Why:** Caching reduces network traffic and improves application responsiveness by serving data from memory.
"""javascript
// Do This: Implement atom-based caching for API calls
import { atom, useAtom } from 'jotai';
const usersAtom = atom(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
return await response.json();
});
function UserListComponent() {
const [users] = useAtom(usersAtom);
if (users instanceof Promise) {
return <p>Loading users...</p>;
}
return (
{users.map(user => (
{user.name}
))}
);
}
// Don't Do This: Fetch users on EVERY render (very bad)!
function BadUserListComponent() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
const fetchUsers = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
setUsers(await response.json());
}
fetchUsers();
}, [])
return (
{users.map(user => (
{user.name}
))}
);
}
"""
### 3.2. Suspendable Atoms
**Do This:** Leverage Jotai's "suspense" feature for asynchronous data fetching using "atom.unstable_enableSuspense = true". Wrap components accessing these atoms with "". This allows for cleaner data fetching logic and automatic handling of loading states.
**Don't Do This:** Manually manage loading states with boolean flags when using asynchronous operations, leading to more verbose and error-prone code.
**Why:** Suspense provides a declarative way to handle asynchronous operations, improving code readability and simplifying data fetching in React components.
"""javascript
// Do This: Use suspendable atoms for cleaner async fetching
import { atom, useAtom } from 'jotai';
import React from 'react';
atom.unstable_enableSuspense = true;
const userAtom = atom(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return await response.json();
});
function UserProfile() {
const [user] = useAtom(userAtom);
return (
{user.name}
<p>Email: {user.email}</p>
);
}
function App() {
return (
Loading user profile...<p></p>}>
);
}
// Don't Do This: Manual loading states + error state is complex
const badUserAtom = atom({ loading: false, data: null, error: null });
function BadUserProfile() {
const [state, setState] = useAtom(badUserAtom);
React.useEffect(() => {
setState(prev => ({ ...prev, loading: true, error: null }));
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => setState({ loading: false, data, error: null }))
.catch(err => setState({ loading: false, error: err, data: null }));
}, []);
if (state.loading) return <p>Loading user...</p>;
if (state.error) return <p>Error: {state.error.message}</p>;
if (!state.data) return <p>No user data</p>; // Initial state
return (
{state.data.name}
<p>Email: {state.data.email}</p>
);
}
"""
### 3.3. Atom Families for Dynamic Data Fetching
**Do This:** Employ atom families ("atomFamily") to manage state for collections of related data, such as fetching data based on IDs.
**Don't Do This:** Create individual atoms for each piece of data in a collection, resulting in a large number of atoms and inefficient updates.
**Why:** Atom families provide a structured way to create and manage atoms dynamically, facilitating efficient data fetching and caching for collections.
"""javascript
// Do This: Use atom families for data fetching based on ID
import { atomFamily, useAtom } from 'jotai';
const userAtomFamily = atomFamily(async (userId: number) => {
const response = await fetch("https://jsonplaceholder.typicode.com/users/${userId}");
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return await response.json();
});
function UserProfile({ userId }: { userId: number }) {
const [user] = useAtom(userAtomFamily(userId));
if (user instanceof Promise) {
return <p>Loading user {userId}...</p>;
}
return (
{user.name}
<p>Email: {user.email}</p>
);
}
// Don't Do This: Create multiple atoms for each user ID.
function BadUserProfile({ userId }: { userId: number }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users/${userId}")
.then(response => response.json())
.then(data => setUser(data));
},[userId]);
if (!user){
return <p>Loading user {userId}...</p>;
}
return (
{user.name}
<p>Email: {user.email}</p>
);
}
"""
## 4. Miscellaneous Optimizations
### 4.1. Proper use of Provider to avoid re-renders
**Do This:** Keep the Provider as low in the tree as possible that still allows access to all relevant components. In other words, scope the provider as closely as possible to the atom usage in your components.
**Don't Do This:** Over-scope a provider at the highest level, causing full app re-renders for state changes only needed in a small section.
**Why:** Jotai relies on React context providers. The documentation says "It’s important to note that whenever the Provider’s value prop is changed, all consumers that are descendants of the Provider will re-render." To address this we want to move providers as low as possible so that we dont change the Provider's value unnecessarily.
"""javascript
// Do This: Low scoped Providers
import { atom, useAtom, Provider } from 'jotai';
import React from 'react';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
function App() {
return (
{/* Provider is low as possible */}
);
}
// Don't Do This: High scoped Provider. Changes here cause massive changes
const globalValueAtom = atom(123);
function BadCounter() {
const [count, setCount] = useAtom(countAtom);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
function BadApp() {
const [globalValue, setGlobalValue] = useAtom(globalValueAtom);
return (
setGlobalValue(parseInt(e.target.value))} />
);
}
"""
### 4.2. Use Jotai Devtools for Profiling
**Do This:** Use the Jotai Devtools chrome extension (or similar) to understand atom updates, render cycles, and dependencies for performance tuning.
**Don't Do This:** Blindly optimize without insights from profilers
By adhering to these performance optimization standards, you can build highly performant and responsive applications with Jotai, ensuring a smooth user experience and efficient resource utilization. This document provides a solid foundation for developing optimized Jotai code and promotes a consistent approach across your team.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Deployment and DevOps Standards for Jotai This document outlines the deployment and DevOps standards for Jotai applications to ensure maintainable, performant, and secure production environments. It focuses on the unique aspects of Jotai within the context of modern JavaScript deployment practices. ## 1. Build Processes, CI/CD, and Production Considerations ### 1.1. Standard: Automate Builds and Deployments. * **Do This:** Implement a CI/CD pipeline to automate build, test, and deployment processes. Use tools like GitHub Actions, GitLab CI, CircleCI, or Jenkins. * **Don't Do This:** Manually build and deploy Jotai applications. **Why:** Automating these processes reduces the risk of human error, improves deployment frequency, and ensures consistency across environments. **Example (GitHub Actions):** """yaml name: Deploy to Production on: push: branches: - main # Or your production branch jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18' # Or your preferred Node.js version - name: Install Dependencies run: npm ci # Or yarn install --frozen-lockfile - name: Build Application run: npm run build - name: Deploy to Production run: | # Replace with your deployment script echo "Deploying to production..." # Example using SSH: # ssh user@your-server "cd /var/www/your-app && git pull origin main && npm install && pm2 restart all" env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} # Store SSH key securely as a GitHub secret """ **Anti-pattern:** Directly editing files on a production server. ### 1.2. Standard: Version Control Everything. * **Do This:** Store all code (including configuration files, infrastructure-as-code definitions, and deployment scripts) in a version control system like Git. Utilize branching strategies (e.g., Gitflow, GitHub Flow) tailored to your team size/complexity. * **Don't Do This:** Keep code or configuration only on local machines. **Why:** Version control provides a history of changes, enables collaboration, and allows for easy rollback in case of errors. **Example (Gitflow):** * "main" branch: Always deployable production code. * "develop" branch: Integration branch for new features. * Feature branches: For individual feature development (branched from "develop"). * Release branches: For preparing a release (branched from "develop"). * Hotfix branches: For fixing bugs in production (branched from "main"). ### 1.3. Standard: Environment-Specific Configuration. * **Do This:** Utilize environment variables to manage configuration settings that vary between development, staging, and production environments. * **Don't Do This:** Hardcode configuration values directly into your Jotai application code. **Why:** Environment variables allow you to configure your application without modifying the code, making deployments easier and more secure. This is particularly important for API keys, database connection strings, and feature flags. **Example:** """javascript // In your Jotai atom definition import { atom } from 'jotai'; const apiUrlAtom = atom(process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'); export { apiUrlAtom }; """ In your ".env" file (or equivalent for your deployment platform): """ NEXT_PUBLIC_API_URL=https://production.example.com/api """ **Technology-specific detail (Next.js):** Use "NEXT_PUBLIC_*" prefix for environment variables that need to be exposed to the client-side JavaScript. **Example (Using a configuration file):** """javascript // config.js const config = { development: { apiUrl: 'http://localhost:3000/api', }, production: { apiUrl: process.env.API_URL || 'https://production.example.com/api', }, }; const env = process.env.NODE_ENV || 'development'; export default config[env]; // In your Jotai atom: import { atom } from 'jotai'; import config from './config'; const apiUrlAtom = atom(config.apiUrl); export { apiUrlAtom }; """ **Anti-pattern:** Committing ".env" files containing sensitive information to version control. Use secret management solutions like Vault or cloud provider secrets managers. ### 1.4. Standard: Use Infrastructure as Code (IaC). * **Do This:** Define and manage your infrastructure (servers, databases, networking) using code. Tools like Terraform, AWS CloudFormation, or Azure Resource Manager are common choices. * **Don't Do This:** Manually provision and configure infrastructure components. **Why:** IaC allows you to automate infrastructure provisioning, ensuring consistency, repeatability, and auditability. **Example (Terraform):** """terraform resource "aws_instance" "example" { ami = "ami-0c55b5c63b456d001" # Replace with your AMI instance_type = "t2.micro" tags = { Name = "Jotai-Production-Server" } } output "public_ip" { value = aws_instance.example.public_ip } """ ### 1.5. Standard: Comprehensive Logging and Monitoring. * **Do This:** Implement robust logging and monitoring to track application health, performance, and errors. Use tools like Sentry, Datadog, New Relic, or the ELK stack (Elasticsearch, Logstash, Kibana). Pay special attention to Jotai-related state changes that are critical for business logic. * **Don't Do This:** Rely solely on console logs or manual error tracking. **Why:** Logging and monitoring allow you to quickly identify and resolve issues, optimize application performance, and ensure application availability. **Example (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 this value in production environment: process.env.NODE_ENV, }); // Wrap your application with Sentry.ErrorBoundary to capture React errors function App() { return ( <Sentry.ErrorBoundary fallback={"An error occurred"}> {/* Your application code */} </Sentry.ErrorBoundary> ); } export default App; //Example capturing custom errors try { // Your Jotai related code that might throw an error // For example: // const updateResult = await updateData(get(dataAtom)); //if(!updateResult.success){ // throw new Error("Failed to Update Data") //} } catch (error) { Sentry.captureException(error); } """ **Jotai Specific Monitoring:** Consider logging key Jotai atom state changes, especially for atoms storing critical data, to understand user behavior or debug unexpected application states. You can use the "useAtom" hook's returned "setAtom" function to trigger logging alongside state updates. Carefully consider the performance impact before logging excessively. ### 1.6. Standard: Performance Optimization for Production. * **Do This:** Optimize your Jotai application for production performance. This includes code splitting, lazy loading, memoization, and minimizing bundle sizes. Carefully consider the performance implications of derived atoms, especially those with computationally expensive calculations. * **Don't Do This:** Deploy unoptimized code to production. **Why:** Optimized code leads to faster load times, improved user experience, and reduced server costs. **Example (Code Splitting with "React.lazy"):** """javascript import React, { lazy, Suspense } from 'react'; const MyComponent = lazy(() => import('./MyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> ); } export default App; """ **Jotai Specific Performance:** Use "atomFamily" judiciously for creating dynamic atoms. Overuse can lead to performance issues. Consider atom scoping to limit the scope of atom updates. **Example (Memoize derived atoms):** """javascript import { atom } from 'jotai' import { useMemo } from 'react' const baseAtom = atom(1) const derivedAtom = atom((get) => { const value = get(baseAtom) console.log('recomputed') // This will log only when baseAtom changes return value * 2 }) export const useExpensiveDerivedValue = () => { const value = useAtomValue(derivedAtom) //No useMemo needed because the atom itself only recomputes when baseAtom changes return value; } """ **Anti-pattern:** Using "atom" for large, complex objects that are frequently updated. Consider breaking down the object into smaller, more manageable atoms or using "atomFamily" with keys for individual properties. ## 2. Security Best Practices Specific to Jotai ### 2.1. Standard: Prevent Client-Side Secrets Exposure * **Do This:** Avoid storing sensitive information (API keys, tokens) directly in Jotai atoms that are accessible client-side. Use backend APIs to handle authentication and authorization. * **Don't Do This:** Place secrets directly in Jotai atoms that are then used in client-side logic. **Why:** Client-side code is easily inspectable, making it vulnerable to secrets exposure. **Example: Securing API Calls** Instead of storing an API key in a Jotai atom: """javascript //BAD: const apiKeyAtom = atom("YOUR_API_KEY"); // Don't do this! Exposes the key. const dataAtom = atom(async (get) => { const apiKey = get(apiKeyAtom); const response = await fetch("/data", { headers: { 'X-API-Key': apiKey } }); return await response.json(); }); """ Use a server to broker the API request: """javascript // GOOD: const dataAtom = atom(async (get) => { const response = await fetch("/api/data"); // Calls a serverless function. return await response.json(); }); """ Create "/api/data" endpoint (example for Next.js): """javascript // pages/api/data.js (Next.js example) export default async function handler(req, res) { const apiKey = process.env.API_KEY; const response = await fetch("https://external-api.com/data", { headers: { 'X-API-Key': apiKey } }); const data = await response.json(); res.status(200).json(data); } """ ### 2.2. Standard: Validate and Sanitize Data. * **Do This:** Validate and sanitize all data retrieved from external sources or user input before storing it in Jotai atoms. This prevents XSS and other injection attacks. Tools like "validator.js" or libraries like Zod and Yup can be leveraged. * **Don't Do This:** Directly store unsanitized data in atoms, especially data that will be rendered in the UI. **Why:** Unvalidated data can be exploited to inject malicious code into your application. **Example: Data Validation with Zod** """javascript import { atom } from 'jotai'; import { z } from 'zod'; // Define the schema for your data. const UserSchema = z.object({ name: z.string().min(1).max(50), email: z.string().email(), age: z.number().int().positive().optional() // can be omitted or nullable. (default undefined) }); const userDataAtom = atom(async () => { const response = await fetch('/api/user-data'); const data = await response.json(); try { // Parse the data with the schema. return UserSchema.parse(data); } catch (error) { // Handle validation errors. console.error("Validation error:", error.errors); throw error // Consider using a safe default or an error state atom. } }); export { userDataAtom }; """ ### 2.3. Standard: Secure API key/sensitive data transmission * **Do This:** When fetching external API data, make your API calls via a secure channel (HTTPS) and never log sensitive information. Ensure any API keys or tokens used are securely stored server-side and injected into the requests at the backend, not the frontend. * **Don't Do This:** Hardcode API keys in Jotai atoms, transmit secrets in query parameters, or log API request/response bodies containing sensitive data. **Why:** This prevents interception of sensitive data during transmission. ### 2.4. Standard: Implement Rate Limiting * **Do This:** Apply rate limiting for your API endpoints to prevent abuse and DDoS attacks. This is especially pertinent if your Jotai app directly interacts with external APIs by making user input drive API requests. * **Don't Do This:** Expose API endpoints without rate limiting, as your application could be abused or overwhelmed with requests. **Why:** Safeguards your backend servers from abuse and malicious actors. ### 2.5. Standard: Protect against CSRF attacks. * **Do This:** If your Jotai app performs state-changing operations (e.g., updates to user profiles) , implement CSRF protection to prevent malicious websites from forging requests on behalf of logged-in users. * **Don't Do This:** Assume your API is inherently protected against CSRF, as client-side state management can influence the attack surface. **Why:** Prevents unauthorized actions being performed on behalf of the user. ## 3. Jotai-Specific Deployment Considerations ### 3.1. Code Splitting & Lazy Loading with Atoms Jotai's flexibility can sometimes lead to large atom graphs. Consider these points for optimizing bundle sizes: * **Lazy Atom Initialization:** Defer atom computations if initial values are not crucial during initial page load. This reduces the initial JS payload. * **Tree Shaking:** Ensure your build tools are correctly tree-shaking unused Jotai functions and atoms. * **Dynamic Imports in Derived Atoms:** If a derived atom uses code from a large library, consider using dynamic imports ("import()") within the derived atom's computation function to load the library on demand. ### 3.2. Server-Side Rendering (SSR) with Jotai When using Jotai with SSR frameworks (like Next.js): * **Atom Initialization:** Ensure atoms are properly initialized on the server before being used by the client. The server side and client side should be in sync. * **State Hydration:** Use Jotai's utilities to properly hydrate the client-side state with the server-rendered state. * **Avoid Window Reference:** Be wary of referencing "window" directly within atom initializers, as "window" is not available on the server. Defer such logic to "useEffect" hooks in React components. * **Memory Leaks:** Server environments should have well maintained memory management strategies (e.g. using TTL) to prevent memory leaks from too many atoms persisting in memory. ### 3.3. Testing with Jotai * **Unit Tests:** Test individual atoms in isolation to verify their behavior and state transitions. Mock external dependencies (API calls, etc.) properly. * **Integration Tests:** Write integration tests to ensure that atoms interact correctly with each other and with React components. * **End-to-End (E2E) Tests:** Use E2E testing frameworks (like Cypress or Playwright) to test the entire application flow, including Jotai state management, in a real browser environment. By adhering to these deployment and DevOps standards, development teams can build and deploy Jotai applications that are reliable, performant, secure, and easily maintainable. Remember to tailor these guidelines to your specific project requirements and technology stack.
# Component Design Standards for Jotai This document outlines component design standards when using Jotai for state management in React applications. The goal is to promote reusable, maintainable, and performant components that leverage Jotai effectively. ## 1. Principles of Jotai Component Design ### 1.1. Single Responsibility Principle * **Standard:** Each component should have a single, well-defined responsibility related to managing and displaying Jotai-related data. * **Why:** Enforces modularity, making components easier to understand, test, and reuse. Changes to one aspect of the application are less likely to impact other components. * **Do This:** Extract complex logic into separate functions or custom hooks. Focus a component on a specific UI element or state-related task. """jsx // Good: Focused component import { useAtom } from 'jotai'; import { countAtom } from './atoms'; // Atoms should be in their own module function CounterDisplay() { const [count] = useAtom(countAtom); return <div>Count: {count}</div>; } export default CounterDisplay; """ * **Don't Do This:** Create "god components" that handle multiple unrelated tasks, leading to code bloat and poor maintainability. """jsx // Bad: Component handling too much import { useAtom } from 'jotai'; import { countAtom } from './atoms'; function ComplexComponent() { const [count, setCount] = useAtom(countAtom); const increment = () => setCount(prev => prev + 1); const decrement = () => setCount(prev => prev - 1); // ... other unrelated logic here return ( <div> Count: {count} <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> {/*Other components and Logic unrelated to count */} </div> ); } export default ComplexComponent; """ ### 1.2. Separation of Concerns * **Standard:** Separate UI concerns from state management and business logic. * **Why:** Improves code readability, testability, and maintainability. Allows for independent changes without affecting other parts of the application. * **Do This:** Use custom hooks to encapsulate state logic, keeping components focused on rendering. """jsx // Good: Custom hook for state logic import { useAtom } from 'jotai'; import { countAtom } from './atoms'; import { useCallback } from 'react'; function useCounter() { const [count, setCount] = useAtom(countAtom); const increment = useCallback(() => setCount(prev => prev + 1), [setCount]); const decrement = useCallback(() => setCount(prev => prev - 1), [setCount]); return { count, increment, decrement }; } function CounterDisplay() { const { count } = useCounter(); return <div>Count: {count}</div>; } function CounterButtons() { const { increment, decrement } = useCounter(); return( <> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </> ); } export {CounterDisplay, CounterButtons}; """ * **Don't Do This:** Mix UI rendering logic with complex state updates and side effects directly within the component. """jsx // Bad: Mixing UI and state logic import { useAtom } from 'jotai'; import { countAtom } from './atoms'; function CounterComponent() { const [count, setCount] = useAtom(countAtom); const increment = () => { // Complex logic here setCount(prev => prev + 1); // More complex logic }; return ( <div> Count: {count} <button onClick={increment}>Increment</button> </div> ); } export default CounterComponent; """ ### 1.3. Reusability * **Standard:** Design components to be reusable across multiple contexts and parts of the application. * **Why:** Reduces code duplication, saving development time and ensuring consistency. Makes it easier to maintain and update the application. * **Do This:** Use props to configure component behavior and appearance. Make components as generic as possible while still fulfilling their core responsibility. * **Don't Do This:** Hardcode values specific to a single use case or tightly couple a component to a specific part of the application. """jsx // Good: Reusable component with props import { useAtom } from 'jotai'; function LabeledValue({ atom, label }) { const [value] = useAtom(atom); return ( <div> {label}: {value} </div> ); } export default LabeledValue; """ """jsx // Usage example: import { atom } from 'jotai'; import LabeledValue from './LabeledValue'; const priceAtom = atom(10); const quantityAtom = atom(5); function MyComponent() { return ( <> <LabeledValue atom={priceAtom} label="Price" /> <LabeledValue atom={quantityAtom} label="Quantity" /> </> ); } """ ### 1.4 Composition over Inheritance * **Standard:** Favor composition by creating small, specialized components and combining them to achieve complex UI behaviors. * **Why:** Composition leads to more flexible and maintainable code. It avoids the rigid structure of inheritance, which can lead to tight coupling and the fragile base class problem. * **Do This:** Create components that accept children (using "props.children"). Use render props or function as children to customize component behavior. """jsx import { useAtom } from 'jotai'; import { useState, useCallback } from 'react'; // Controlled Input with Reset function ControlledInput({ atom, label }) { const [value, setValue] = useAtom(atom); const [tempValue, setTempValue] = useState(value); const handleChange = useCallback((e) => { setTempValue(e.target.value); }, [setTempValue]); const handleReset = useCallback(() => { setTempValue(value); // Revert to the atom's value }, [value]); const handleBlur = useCallback(() => { setValue(tempValue); // Update the atom's value when the input loses focus }, [setValue, tempValue]); return ( <div> <label>{label}: </label> <input type="text" value={tempValue} onChange={handleChange} onBlur={handleBlur} /> <button onClick={handleReset}>Reset</button> </div> ); } export default ControlledInput; """ * **Don't Do This:** Create deeply nested component hierarchies using inheritance-like patterns. Attempt to shoehorn unrelated features into a single component. ### 1.5 Minimize Boilerplate * **Standard:** Leverage Jotai's features to reduce boilerplate code associated with managing state in components. * **Why:** Less boilerplate improves readability, reduces the likelihood of errors, and makes it easier to refactor code. * **Do This:** Use "useAtom" for simple state access and updates. Use derived atoms for computed values, avoiding redundant logic in components. Consider atom families for dynamic atom creation. """jsx // Good: Concise state management with useAtom import { useAtom } from 'jotai'; import { countAtom } from './atoms'; function Counter() { const [count, setCount] = useAtom(countAtom); return ( <div> <button onClick={() => setCount(prev => prev - 1)}>-</button> <span>{count}</span> <button onClick={() => setCount(prev => prev + 1)}>+</button> </div> ); } """ * **Don't Do This:** Manually manage state updates using "useState" and "useEffect" when Jotai provides a more elegant and concise solution. ### 1.6 Avoid Direct Atom Mutation Within Components * **Standard:** Components should not directly mutate the values within atoms. Always use the "setAtom" function returned by "useAtom" or custom update functions for state changes. * **Why:** Directly mutating atom values can lead to unexpected behavior, race conditions, and difficulties in debugging. Jotai relies on controlled updates through "setAtom" to trigger re-renders and track changes. * **Do This:** Use the "setAtom" function in conjunction with a callback function to update the atom's value based on its previous value. """jsx // Good: Updating atom value correctly import { useAtom } from 'jotai'; import { countAtom } from './atoms'; function IncrementButton() { const [count, setCount] = useAtom(countAtom); const increment = () => { setCount(prevCount => prevCount + 1); // Correct way to update }; return <button onClick={increment}>Increment</button>; } """ * **Don't Do This:** Directly modify the value of the atom without using "setAtom". """jsx // Bad: Directly modifying atom value import { useAtom } from 'jotai'; import { countAtom } from './atoms'; function BadIncrementButton() { const [count, setCount] = useAtom(countAtom); const increment = () => { count = count + 1; // Incorrect way to update (direct mutation) setCount(count); //This will compile, but violates the principal and may lead to unexpected behavior }; return <button onClick={increment}>Increment</button>; } """ ### 1.7 Testability * **Standard:** Components using Jotai should be easily testable in isolation. * **Why:** Testable components lead to more reliable applications and easier refactoring. Comprehensive testing catches bugs early in the development process. * **Do This:** Mock Jotai atoms using testing libraries like "jest" or "vitest". Write unit tests that verify the component's behavior when interacting with Jotai atoms. Use testing libraries that work well with React Hooks, such as "@testing-library/react-hooks". * **Don't Do This:** Create components with complex external dependencies that make unit testing difficult. ## 2. Jotai-Specific Patterns for Component Design ### 2.1. Atom Families for Dynamic Components * **Standard:** Use atom families when creating components that render lists or collections of items, each with its own independent state. * **Why:** Atom families allow you to dynamically create and manage atoms for each item in the list, avoiding conflicts and improving performance. * **Do This:** Define an atom family that takes a unique identifier as a parameter. Use this identifier to access the atom within the component. """jsx import { atomFamily, useAtom } from 'jotai'; // Define an atom family for todo items const todoAtomFamily = atomFamily((id) => atom({ id: id, text: '', completed: false })); function TodoItem({ id }) { const [todo, setTodo] = useAtom(todoAtomFamily(id)); const handleChange = (e) => { setTodo(prev => ({ ...prev, text: e.target.value })); }; return ( <li> <input type="text" value={todo.text} onChange={handleChange} /> </li> ); } function TodoList() { const todoIds = ['todo-1', 'todo-2', 'todo-3']; // Replace with dynamic IDs return ( <ul> {todoIds.map(id => ( <TodoItem key={id} id={id} /> ))} </ul> ); } export default TodoList; """ * **Don't Do This:** Try to manage the state of all items in a list using a single atom, which can lead to performance issues and complex update logic. ### 2.2. Derived Atoms for Computed Values * **Standard:** Use derived atoms to create computed values that depend on other atoms. * **Why:** Derived atoms automatically update whenever their dependencies change, ensuring that the UI always reflects the latest computed values. Avoids redundant calculations and keeps components focused on rendering. * **Do This:** Create derived atoms using the "atom" function, providing a getter function that calculates the value based on other atoms. """jsx import { atom, useAtom } from 'jotai'; const priceAtom = atom(10); const quantityAtom = atom(2); // Derived atom for the total price const totalPriceAtom = atom( (get) => get(priceAtom) * get(quantityAtom) ); function PriceDisplay() { const [totalPrice] = useAtom(totalPriceAtom); return <div>Total Price: ${totalPrice}</div>; } export default PriceDisplay; """ * **Don't Do This:** Perform the same calculations repeatedly within multiple components or use "useEffect" to manually update computed values. ### 2.3 Pre-fetching Jotai Atoms * **Standard:** Consider pre-fetching Jotai atoms on the server-side or during initial client-side load to improve initial rendering performance. * **Why:** Pre-fetching reduces the time it takes for the UI to display data, leading to a better user experience. Especially useful for critical data required for the initial view. This might involve fetching data from an API and then setting the initial atom value. * **Do This:** Use the approach that aligns with your framework (Next.js, Remix, etc) and hydrate the Jotai store. """jsx // Example with Next.js - getServerSideProps import { atom, useAtom } from 'jotai'; const dataAtom = atom(null); function MyComponent() { const [data] = useAtom(dataAtom); if (!data) { return <div>Loading...</div>; } return <div>Data: {data.name}</div>; } export async function getServerSideProps() { // Fetch data from an API const res = await fetch('https://api.example.com/data'); const data = await res.json(); return { props: { initialData: data, }, }; } MyComponent.getInitialProps = async ({ initialData }) => { // Initialize the Jotai atom with the fetched data dataAtom.write(initialData); // Needs jotai scope for client side return { initialData }; }; export default MyComponent; """ * **Don't Do This:** Fetch data only when the component mounts, leading to a delayed initial rendering. ## 3. Performance Optimization ### 3.1. Atom Scope and Granularity * **Standard:** Define atoms with the appropriate scope and granularity to minimize unnecessary re-renders. * **Why:** Too broad an atom scope can cause excessive re-renders of unrelated components. Too granular atoms can lead to complex update logic and potential performance overhead. Find the right balance for your application. * **Do This:** Group related pieces of state into a single atom when they often change together. Split large atoms into smaller, more focused atoms if they are updated independently. """jsx // Good: Grouping related state import { atom, useAtom } from 'jotai'; const userAtom = atom({ name: 'John Doe', email: 'john.doe@example.com', profilePicture: 'url-to-image', }); function UserProfile() { const [user] = useAtom(userAtom); return ( <div> <img src={user.profilePicture} alt="Profile" /> <div>Name: {user.name}</div> <div>Email: {user.email}</div> </div> ); } """ * **Don't Do This:** Create a single, global atom for managing all application state or create an excessive number of small, tightly coupled atoms. ### 3.2. Selective Updates with "useUpdateAtom" * **Standard Use:** Consider using "useUpdateAtom" when you only need to trigger an update without reading the atom's value in a component. * **Why:** "useUpdateAtom" avoids subscribing the component to the atom's value, potentially reducing unnecessary re-renders. * **Do This:** Use "useUpdateAtom" in components that only dispatch updates to the atom state but don't directly render the atom's value. """jsx import { useAtom, useUpdateAtom } from 'jotai'; import { countAtom } from './atoms'; function IncrementButton() { const updateCount = useUpdateAtom(countAtom); const increment = () => { updateCount(prev => prev + 1); }; return <button onClick={increment}>Increment</button>; } function CounterDisplay() { const [count] = useAtom(countAtom); return <div>Count: {count}</div>; } export {IncrementButton, CounterDisplay}; """ ### 3.3 Immutability is Key * **Standard:** Always update atoms with immutable data structures. * **Why:** Immutability ensures that Jotai can efficiently detect changes and trigger re-renders only when necessary. Avoids unexpected side effects caused by mutating data. * **Do This:** Use the spread operator ("...") or immutable libraries like Immer to create new objects or arrays when updating atom values. """jsx // Good: Immutable update with spread operator import { useAtom } from 'jotai'; import { todosAtom } from './atoms'; function ToggleTodo({ id }) { const [todos, setTodos] = useAtom(todosAtom); const toggleComplete = () => { setTodos(prevTodos => prevTodos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }; return <button onClick={toggleComplete}>Toggle</button>; } """ * **Don't Do This:** Directly modify the properties of atom values, as this can lead to inconsistencies and performance issues. ## 4. Security Considerations ### 4.1. Sensitive Data Handling * **Standard:** Avoid storing sensitive data directly in Jotai atoms, especially on the client-side. * **Why:** Client-side state is generally accessible and can be vulnerable to security exploits. Sensitive data should be managed securely on the server-side. * **Do This:** Store sensitive data in secure storage mechanisms (e.g., cookies, local storage with encryption) or manage it on the server-side with appropriate authentication and authorization controls. ### 4.2. Input Validation * **Standard:** Validate any user inputs before updating Jotai atoms. * **Why:** Prevents malicious data from being stored in the application state, which could lead to security vulnerabilities or unexpected behavior. * **Do This:** Implement input validation logic within components or custom hooks before calling "setAtom". """jsx import { useAtom } from 'jotai'; import { usernameAtom } from './atoms'; function UsernameInput() { const [username, setUsername] = useAtom(usernameAtom); const handleChange = (e) => { const newValue = e.target.value; if (newValue.length <= 20 && /^[a-zA-Z0-9]+$/.test(newValue)) { setUsername(newValue); } else { // Show an error message or prevent the update alert('Invalid username'); } }; return <input type="text" value={username} onChange={handleChange} />; } """ ## 5. Code Style and Formatting ### 5.1 Linter Configuration * **Standard:** Enforce code style consistency through linters (e.g., ESLint) following established Javascript and React standards. * **Why:** Consistent code style improves readability and reduces the cognitive load for developers working on the project. * **Do This:** Configure your project with ESLint and Prettier, using a shareable configuration that defines rules for code style, formatting, and best practices. Apply linting rules to all Jotai-related code. ### 5.2 Atom Definition Locations * **Standard:** Keep all atom definitions in a dedicated directory (e.g., "src/atoms"). * **Why:** Centralized atom definitions make it easier to find, manage, and reason about the application's state. * **Do This:** Group atoms related to specific features or modules into separate files within the "atoms" directory. """ src/ atoms/ count.js user.js settings.js """ ## 6. Jotai Ecosystem Tools ### 6.1 jotai/immer * **Standard:** Utilize if reducers are required for very complex object manipulation * **Why:** Immer removes boilerplate code and ensures that immutable data structures are used, even when performing deep updates. * **Do This:** Install "@jotai/immer" and leverage the "useImmerAtom" hook: """jsx import { useImmerAtom } from 'jotai/immer' import { atom } from 'jotai' const todosAtom = atom([ { id: 1, text: 'Learn Jotai', done: false }, { id: 2, text: 'Write some code', done: true }, ]) function Todos() { const [todos, updateTodos] = useImmerAtom(todosAtom) const toggleTodo = (id) => { updateTodos(draft => { const todo = draft.find(t => t.id === id); if (todo) { todo.done = !todo.done; } }); } return ( <ul> {todos.map(todo => { <li key={todo.id} onClick={() => toggleTodo(todo.id)}>{todo.text}</li> })} </ul> ) } """ This document should be treated as a living document, subject to revisions and additions as Jotai evolves and new best practices emerge.
# Core Architecture Standards for Jotai This document defines the core architectural standards for Jotai-based applications. It outlines the fundamental principles, project structure, and organization guidelines necessary for building maintainable, scalable, and performant applications with Jotai. ## 1. Fundamental Architectural Patterns Jotai, being a primitive and unopinionated state management library, allows for flexibility in architectural choices. However, certain patterns synergize particularly well with its approach. ### 1.1 Functional Core, Imperative Shell **Description:** This architecture separates the pure, side-effect-free business logic (the "core") from the state management and UI interactions (the "shell"). Jotai atoms act as the conduit between the core and the shell, providing a reactive interface for data updates. **WHY:** This approach improves testability, maintainability, and overall code clarity. By separating business logic from side effects, you create a predictable and isolated system. **Do This:** * Encapsulate complex business logic within pure functions. * Use Jotai atoms to manage the state used by these functions. * Connect UI components to these atoms to trigger and react to state changes. **Don't Do This:** * Embed business logic directly within UI components or side effects. * Mutate state directly within business logic. **Example:** """javascript // Core: Pure function for calculating total price with tax const calculateTotalPrice = (price, taxRate) => price * (1 + taxRate); // Atom for storing item price import { atom } from 'jotai'; export const itemPriceAtom = atom(100); // Atom for storing tax rate export const taxRateAtom = atom(0.08); // Atom for deriving the total price export const totalPriceAtom = atom((get) => { const price = get(itemPriceAtom); const taxRate = get(taxRateAtom); return calculateTotalPrice(price, taxRate); }); // Shell: React component using the totalPriceAtom import { useAtom } from 'jotai'; const PriceDisplay = () => { const [totalPrice] = useAtom(totalPriceAtom); return ( <div> Total Price: ${totalPrice.toFixed(2)} </div> ); }; export default PriceDisplay; """ ### 1.2 Atomic Design **Description:** Decompose the UI into reusable and composable components representing different levels of granularity (atoms, molecules, organisms, templates, pages). Jotai atoms manage the state associated with these components. **WHY:** Atomic design promotes modularity, reusability, and consistency across the application. Jotai simplifies state management at each level of the component hierarchy. **Do This:** * Start with atomic components representing the smallest UI elements. * Compose atoms into molecules, organisms, and templates. * Manage state at the appropriate level of granularity with Jotai atoms. **Don't Do This:** * Create monolithic components that handle too much state and logic. **Example:** """javascript // Atom (smallest unit) import { atom, useAtom } from 'jotai'; export const buttonTextAtom = atom("Click Me"); const ButtonText = () => { const [text, setText] = useAtom(buttonTextAtom); return ( <button onClick={() =>setText('Clicked!')}> {text} </button> ); } export default ButtonText; // Molecule (group of atoms) import ButtonText from './ButtonText'; const CallToActionButton = () => { return ( <div> <h3>Ready to get started?</h3> <ButtonText/> // Use Atom here too, but composed </div> ) } export default CallToActionButton; """ ## 2. Project Structure and Organization A well-defined project structure is crucial for maintainability, especially as the application grows. ### 2.1 Feature-Based Organization **Description:** Structure your project around features rather than technical layers. Each feature should have its own directory containing components, logic, and Jotai atoms. **WHY:** This organization makes it easier to locate and understand code related to a specific feature. It promotes modularity and reduces code duplication. **Do This:** * Create a directory for each feature. * Place all related code for that feature within the directory (components, atoms, utilities). **Don't Do This:** * Organize code by technical layers (e.g., "components," "atoms," "utils") at the top level. **Example:** """ src/ ├── features/ │ ├── auth/ # Authentication feature │ │ ├── components/ │ │ │ ├── Login.jsx │ │ │ └── Register.jsx │ │ ├── atoms/ │ │ │ ├── authAtoms.js │ │ │ └── userAtom.js │ │ ├── utils/ │ │ │ └── authApi.js │ │ └── index.js # Entry point for the feature │ ├── dashboard/ # Dashboard feature │ │ ├── ... │ └── ... ├── components/ # Shared UI components ├── App.jsx └── index.js """ ### 2.2 Atom Colocation **Description:** Place Jotai atoms close to the components that use them. This promotes discoverability and reduces the distance between state and UI. **WHY:** Colocating atoms makes it easier to understand the relationship between state and UI. It also simplifies refactoring and code reuse. **Do This:** * Define atoms within the same file as the component that uses them (if only used by that component). * Create a separate "atoms" directory within the feature directory for atoms used by multiple components. **Don't Do This:** * Create a single, global "atoms" directory for all atoms in the application. **Example:** """javascript // components/auth/Login.jsx import { atom, useAtom } from 'jotai'; import { useState } from 'react'; // Atom specific to Login component const loginErrorAtom = atom(''); const Login = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loginError, setLoginError] = useAtom(loginErrorAtom); const handleSubmit = async (e) => { e.preventDefault(); try { // Authentication logic (e.g., calling an API) // If there's an error, set the error atom if (!username || !password) { setLoginError('Invalid Username or Password'); } } catch (error) { setLoginError('An error occurred during login.'); } }; return ( <form onSubmit={handleSubmit}> {loginError && <p className="error">{loginError}</p>} {/* ... Login form elements ... */} </form> ); }; export default Login; """ ### 2.3 Consistent Naming Conventions **Description:** Establish clear and consistent naming conventions for atoms, components, and other elements. **WHY:** Consistent naming improves code readability and maintainability. **Do This:** * Use "camelCase" for atom names (e.g., "userAtom", "itemPriceAtom"). * Use "PascalCase" for component names (e.g., "Login", "PriceDisplay"). * Suffix atom names with "Atom" to clearly identify them. **Don't Do This:** * Use inconsistent or ambiguous naming conventions. ## 3. Jotai-Specific Best Practices Leveraging Jotai's features effectively is essential for building performant and maintainable applications. ### 3.1 Derived Atoms for Complex State **Description:** Use derived atoms to compute complex state based on other atoms. This promotes data normalization and reduces redundant calculations. **WHY:** Derived atoms automatically update when their dependencies change. This ensures that the computed state is always consistent. **Do This:** * Create derived atoms for any state that can be computed from other atoms. * Use the "get" function within the derived atom's function to access the values of its dependencies. **Don't Do This:** * Duplicate calculations in multiple components. * Manually update derived state when its dependencies change. **Example:** """javascript import { atom } from 'jotai'; const quantityAtom = atom(2); const unitPriceAtom = atom(25); const totalPriceAtom = atom((get) => { const quantity = get(quantityAtom); const unitPrice = get(unitPriceAtom); return quantity * unitPrice; }); """ ### 3.2 "useAtomValue" and "useSetAtom" Hooks **Description:** Use the "useAtomValue" hook to only read the value of an atom, and "useSetAtom"to update its value. This optimizes component re-renders. **WHY:** "useAtom" hook subscribes to the atom and triggers re-renders whenever the atom's value changes but it includes the write function as well. When you only need to read the value of an atom, "useAtomValue" prevents unnecessary re-renders when the write function changes in the atom. Simmilarly when you only need the write function use "useSetAtom". **Do This:** * Use "useAtomValue" when a component only needs to read the value of an atom - improves rendering performance. * Use "useSetAtom" when a component needs to update the value of an atom, without the need to the value itself. * Use "useAtom" when the comoponent needs both reading and writing capabilities. **Don't Do This:** * Always use "useAtom" for both reading and writing, especially in large components. **Example:** """javascript import { atom, useAtomValue, useSetAtom } from 'jotai'; const counterAtom = atom(0); // Component that only displays the counter value const CounterDisplay = () => { const counter = useAtomValue(counterAtom); // Only reads value return <div>Counter: {counter}</div>; }; // Component that only increments the counter const CounterIncrementer = () => { const setCounter = useSetAtom(counterAtom); // Only write function const increment = () => { setCounter((prev) => prev + 1); }; return <button onClick={increment}>Increment</button>; }; export {CounterDisplay, CounterIncrementer} """ ### 3.3 Atom Families for Dynamic Data **Description:** Use atom families to manage collections of related atoms identified by a key. This provides a powerful way to handle dynamic data. **WHY:** Atom families simplify the management of data where the number of atoms is not known in advance. They also provide a mechanism to create and dispose of atoms on demand. **Do This:** * Use "atomFamily" to create atoms with a unique key. * Access individual atoms within the family by passing the key to the resulting function. **Don't Do This:** * Manually manage collections of atoms. **Example:** """javascript import { atomFamily, useAtom } from 'jotai'; // Atom family for managing todo items by ID const todoAtomFamily = atomFamily((id) => atom({ id, text: '', completed: false })); const TodoItem = ({ id }) => { const [todo, setTodo] = useAtom(todoAtomFamily(id)); const handleChange = (e) => { setTodo({...todo, text: e.target.value}) }; return ( <li> <input type="text" value={todo.text} onChange={handleChange} /> </li> ); }; """ ### 3.4 Understanding and leveraging "scope" in Jotai **Description**: Jotai supports multiple stores created by the "createStore()" function. Each store is independent and can be useful for complex scenarios like code splitting or micro-frontends. The "scope" prop in the providers allows you to bind to specific stores. **Why**: Scopes provide isolation between different parts of your applications, ensuring states don't accidentally bleed into undesired components. Multiple stores can coexist. **Do This**: *When using code-splitting techniques where a portion of the application has its atom set separate to the main app. *For plugins within a larger application where plugin-specific global states should be contained. *When using SSR where you want a uniquely seeded store per request. **Don't Do This:** *Creating multiple stores without being extremely mindful about their relation to each other - it can add complexity to debugging. **Example:** """javascript import { atom, useAtom, Provider } from 'jotai' import { createStore } from 'jotai' const exampleAtom = atom('Default Value'); const customStore = createStore() const ComponentUsingDefaultStore = () => { const [value, setValue] = useAtom(exampleAtom) return ( <div> From Default Store: {value} <button onClick={() => setValue('New Default Value')}>Update</button> </div> ) } const ComponentUsingCustomStore = () => { const [value, setValue] = useAtom(exampleAtom, { store: customStore }) return ( <div> From Custom Store: {value} <button onClick={() => setValue('New Custom Value')}>Update</button> </div> ) } const App = () => { return ( <div> <ComponentUsingDefaultStore /> <Provider store={customStore}> <ComponentUsingCustomStore /> </Provider> </div> ) } export default App; """ ### 3.5 Using "withImmutability" for debugging in development Jotai is unopinionated about state mutations, but React can behave unpredictably if state is mutated. Although Jotai will detect basic violations of this, it can be hard to detect what component has caused the mutation. Jotai provides "immutableAtom" and "withImmutability" functionality. **WHY:** Improves state management debugging and prevents accidental direct state mutations. **DO THIS:** Use them in development of applications in the outer-most shell function, so all calls to state management are under the correct rules **DON'T DO THIS:** Deploy the application with this on production - as it will impact performance. """javascript // Core: Pure function for calculating total price with tax const calculateTotalPrice = (price, taxRate) => price * (1 + taxRate); // Atom for storing item price import { atom, withImmutability } from 'jotai'; export const itemPriceAtom = atom(100); // Atom for storing tax rate export const taxRateAtom = atom(0.08); // Atom for deriving the total price export const totalPriceAtom = atom((get) => { const price = get(itemPriceAtom); const taxRate = get(taxRateAtom); return calculateTotalPrice(price, taxRate); }); const debugPriceAtom = withImmutability(totalPriceAtom); // Shell: React component using the totalPriceAtom import { useAtom } from 'jotai'; const PriceDisplay = () => { const [totalPrice] = useAtom(debugPriceAtom); // use the debug atom with immutability check return ( <div> Total Price: ${totalPrice.toFixed(2)} </div> ); }; export default PriceDisplay; """ ## 4. Performance Optimization Jotai is designed to be performant, but certain practices can further improve rendering speed and memory usage. ### 4.1 Selective Rendering with "useAtomValue" **Description:** Use "useAtomValue" instead of "useAtom" when a component only needs to read the value of an atom. **WHY:** "useAtomValue" only subscribes to value changes so the component is only re-rendered if the atom's value changes. "useAtom" subscribes to *any* change of the derived atom thus rendering components unnecessarily more often. **Example:** """javascript import { atom, useAtomValue } from 'jotai'; const messageAtom = atom('Hello, World!'); const MessageDisplay = () => { const message = useAtomValue(messageAtom); // Use useAtomValue for reading return <div>{message}</div>; }; """ ### 4.2 Avoiding Unnecessary Atom Updates **Description:** Avoid updating atoms with the same value unnecessarily. **WHY:** Even if the value is the same, updating an atom will trigger re-renders of all subscribing components. **Do This:** * Compare the new value with the current value before updating the atom. * Use "useCallback" to memoize the update function or create it outside of the React Component definition. **Example:** """javascript import { atom, useAtom } from 'jotai'; import { useCallback } from 'react'; const textAtom = atom('Initial Text'); const TextInput = () => { const [text, setText] = useAtom(textAtom); const handleChange = useCallback( //Using useCallback (e) => { const newValue = e.target.value; if (newValue !== text) { setText(newValue); } }, [setText, text] ); return <input type="text" value={text} onChange={handleChange} />; }; export default TextInput; """ ### 4.3 Using "equals" Function in Atoms **Description:** Provide an "equals" function to atoms to customize how changes are detected for complex objects and speed up performance. **WHY:** The default equality check in Jotai (===) may not be sufficient for objects or arrays, leading to unnecessary re-renders when the object's properties remain unchanged. **Do This:** * Use a custom (deep) comparator function for "equals". **Example:** """javascript import { atom } from 'jotai' import isEqual from 'lodash.isequal' // Example: lodash's isEqual const objectAtom = atom({a: 1, b: { c: 2 }}, isEqual); """ ## 5. Security Best Practices While Jotai itself doesn't introduce significant security vulnerabilities, it's crucial to protect the data it manages. ### 5.1 Secure Storage of Sensitive Data **Description:** Avoid storing sensitive data directly in Jotai atoms if the application is rendered on the server, or in environments where the front end state could be observed or manipulated. **WHY:** Jotai atoms are stored in the client-side JavaScript memory. Avoid storing information such as API keys or user credentials directly in atoms, as this could expose sensitive data especially in SSR frameworks. **Do This:** * Store sensitive data using secure storage mechanisms (e.g., cookies with "httpOnly" flag, local storage with encryption). * Only store references (e.g., user ID) in Jotai atoms. * If necessary, encrypt sensitive data before storing it in Jotai atoms. ### 5.2 Input Validation and Sanitization **Description:** Validate and sanitize all user inputs before storing them in Jotai atoms. **WHY:** This protects against cross-site scripting (XSS) and other injection attacks. **Do This:** * Use a validation library (e.g., "zod", "yup") to validate user inputs. * Use a sanitization library (e.g., "DOMPurify") to sanitize user inputs before storing them. """javascript import { atom, useAtom } from 'jotai'; import DOMPurify from 'dompurify'; import { z } from "zod" const userInputAtom = atom(''); const schema = z.string().min(3).max(20); const SafeInput = () => { const [userInput, setUserInput] = useAtom(userInputAtom); const handleChange = (e) => { const dirty = e.target.value; const clean = DOMPurify.sanitize(dirty); const result = schema.safeParse(clean); if (result.success) { setUserInput(result.data) } else { // handle error validation } }; return <input type="text" value={userInput} onChange={handleChange} />; }; """ ## 6. Modern Approaches and Patterns Jotai's flexibility allows you to adopt modern patterns for state management. ### 6.1 Jotai with TypeScript **Description:** Use TypeScript with Jotai to provide static typing and improve code maintainability. **WHY:** TypeScript helps catch errors early and improves code readability. **Do This:** * Define types for atoms and the data they hold. * Use generic types for atom families. **Example:** """typescript import { atom } from 'jotai'; interface Todo { id: number; text: string; completed: boolean; } const todosAtom = atom<Todo[]>([]); """ ### 6.2 Jotai with React Context **Description:** While Jotai provides its own provider, you can integrate it with React Context for advanced scenarios like theming or configuration. **WHY:** React Context can be used to provide global configuration or theming data to the application. Jotai atoms can then be used to manage the state within this context. **Do This:** * Create a React Context. * Use Jotai atoms to store the state within the context. * Provide the context to the application. ## 7. Common Anti-patterns and Mistakes Avoiding common mistakes ensures code quality and prevents performance issues. ### 7.1 Over-Reliance on Global State **Description:** Avoid storing all application state in global Jotai atoms. **WHY:** Over-reliance on global state can lead to tight coupling and make it difficult to reason about the application's behavior. **Do This:** * Store state at the appropriate level of granularity. * Use component-level state for data that is only used by a single component. ### 7.2 Direct State Mutation **Description:** Refrain from directly mutating state within Jotai atoms, especially when used alongside React's strict mode settings. **WHY:** Direct state mutations can lead to unexpected behavior and make it difficult to track state changes. **Do This:** * Use immutable updates to modify state. Use functions for setting state that create new objects or arrays instead of modifying old. ### 7.3 Neglecting Atom Cleanup **Description:** Don't forget to clean up atoms that are no longer needed, especially when using atom families. **WHY:** Leaving unused atoms in memory can lead to memory leaks. Avoid using Jotai atoms to hold onto data longer than necessary. By adhering to these architectural standards, developers can build robust, maintainable, and performant applications with Jotai. This document serves as a guide for making informed decisions about project structure, state management, and best practices within the Jotai ecosystem.
# Code Style and Conventions Standards for Jotai This document outlines the coding standards and conventions for developing applications using Jotai. Adhering to these standards will ensure code consistency, readability, maintainability, and performance optimization. This document is tailored for use with modern JavaScript/TypeScript and the latest versions of Jotai. ## 1. General Formatting and Style Consistent code formatting is crucial for readability and collaboration. We adopt these principles, typically enforced via Prettier. ### 1.1. Formatting * **Standard:** Use Prettier to automatically format your code. Configure your editor to format on save. * **Why:** Ensures consistent formatting across the entire codebase, reducing noise and making it easier to focus on code logic. * **Do This:** Integrate Prettier into your development workflow and configure it within your project's ".prettierrc.js" or ".prettierrc.json" file. * **Don't Do This:** Manually format code, as this is error-prone and inconsistent. """javascript // .prettierrc.js module.exports = { semi: false, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, } """ * **Indentation:** Use 2 spaces for indentation. Avoid tabs. * **Why:** Consistent indentation improves readability and avoids rendering issues across different editors and environments. * **Do This:** Configure your editor to use 2 spaces for indentation. * **Don't Do This:** Use tabs or inconsistent spacing. * **Line Length:** Aim for a line length of no more than 120 characters. * **Why:** Code is more readable when you don't have to scroll horizontally. * **Do This:** Break long lines into multiple shorter lines. * **Don't Do This:** Write lines exceeding 120 characters wherever possible. ### 1.2. Whitespace * **Standard:** Use whitespace to improve readability: * Insert a blank line between logical blocks of code. * Insert a blank line before "return" statements. * Add a space after commas and colons. * Add spaces around operators (e.g., "=", "+", "-", "*", "/"). * **Why:** Whitespace visually separates code sections, making it easier to understand the structure and flow, and quickly identify different operations. * **Do This:** """javascript import { atom, useAtom } from 'jotai' const countAtom = atom(0) function Counter() { const [count, setCount] = useAtom(countAtom) return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ) } """ * **Don't Do This:** """javascript import {atom,useAtom} from 'jotai' const countAtom=atom(0) function Counter(){const[count,setCount]=useAtom(countAtom) return(<div><p>Count: {count}</p><button onClick={()=>setCount(count+1)}>Increment</button></div>)} """ ### 1.3. Comments * **Standard:** Write clear and concise comments to explain complex logic or non-obvious code. Do not comment on obvious code. * **Why:** Comments aid in understanding the code's purpose, functionality, and reasoning, especially when revisited later or by other developers. However, too many comments or comments about obvious code, lead to increased cognitive load. * **Do This:** """javascript // Atom representing the user's login status. True if logged in, false otherwise const isLoggedInAtom = atom(false) // Function to handle user login const loginUser = async (username, password) => { // ...authentication logic here... } """ * **Don't Do This:** """javascript const isLoggedInAtom = atom(false) // create an atom const loginUser = async (username, password) => { // ...authentication logic here... } //login function """ ## 2. Naming Conventions Clear and consistent naming is critical for understanding code. ### 2.1. Atoms * **Standard:** Name Jotai atoms using "camelCase" with a suffix indicating the type of data the atom holds (e.g., "userAtom", "isLoadingAtom", "searchResultsAtom"). * **Why:** Clearly identifies variables as Jotai atoms and provides information about their purpose and data type. * **Do This:** """javascript const userAtom = atom(null) //represents a user object const isLoadingAtom = atom(false) //represents a boolean loading state const searchResultsAtom = atom([])//represents array of search results """ * **Don't Do This:** """javascript const user = atom(null) // Ambiguous - is this an atom or a regular variable? const loading = atom(false) // Not descriptive enough const data = atom([]) // Lacks context """ ### 2.2. Components * **Standard:** Name React components using "PascalCase". Use descriptive names reflecting the component's purpose (e.g., "UserProfile", "ProductList", "LoginForm"). * **Why:** Standard React convention for component naming. * **Do This:** """javascript function UserProfile() { // ...component logic... } """ * **Don't Do This:** """javascript function userProfile() { // Incorrect case // ...component logic... } """ ### 2.3. Variables and Functions * **Standard:** Name variables and functions using "camelCase". Use descriptive names that clearly indicate the purpose of the variable or function (e.g., "userName", "calculateTotal", "fetchData"). * **Why:** Improves code readability and makes it easier to understand the function and variable. * **Do This:** """javascript const userName = 'John Doe' function calculateTotal(price, quantity) { return price * quantity } """ * **Don't Do This:** """javascript const un = 'John Doe' // Abbreviated and unclear function calc(p, q) { // Abbreviated and unclear return p * q } """ ### 2.4. Constants * **Standard:** Name constants using "UPPER_SNAKE_CASE". Use descriptive names that clearly indicate the constant's purpose (e.g., "API_URL", "MAX_ITEMS", "DEFAULT_TIMEOUT"). * **Why:** Standard JavaScript convention for constants. * **Do This:** """javascript const API_URL = 'https://api.example.com' const MAX_ITEMS = 10 """ * **Don't Do This:** """javascript const apiUrl = 'https://api.example.com' // Incorrect case const maxItems = 10 // Incorrect case """ ## 3. Jotai-Specific Conventions These conventions are specifically for using Jotai effectively and efficiently. ### 3.1. Atom Declaration * **Standard:** Declare atoms at the top level of your modules or in a dedicated atom file. Avoid declaring atoms inside components unless the atom's scope is strictly limited to that component. * **Why:** Makes atoms easily discoverable and promotes code reuse. Avoids unnecessary atom recreations within components. * **Do This:** """javascript // atoms.js import { atom } from 'jotai' export const countAtom = atom(0) export const userAtom = atom(null) """ """javascript // component.js import { useAtom } from 'jotai' import { countAtom } from './atoms' function Counter() { const [count, setCount] = useAtom(countAtom) // ... } """ * **Don't Do This:** """javascript function Counter() { const countAtom = atom(0) // Atom declared inside the component const [count, setCount] = useAtom(countAtom) // ... } """ ### 3.2. Atom Composition and Derivation * **Standard:** Use Jotai's derived atoms to create complex state logic. Favor derived atoms over "useEffect" for managing derived data. * **Why:** Ensures that derived state is automatically updated whenever its dependencies change. Derived atoms enhance performance by caching results. * **Do This:** """javascript import { atom } from 'jotai' const basePriceAtom = atom(10) const discountAtom = atom(0.2) //Derived atom to calculate the discounted price const discountedPriceAtom = atom((get) => { const basePrice = get(basePriceAtom) const discount = get(discountAtom) return basePrice * (1 - discount) }) //Example of writable derived atom to update underlying states const nameAtom= atom('john'); const lastNameAtom = atom('doe'); const fullNameAtom = atom( (get) => "${get(nameAtom)} ${get(lastNameAtom)}", (get, set, newFullName) => { const [firstName, lastName] = newFullName.split(' '); set(nameAtom, firstName); set(lastNameAtom, lastName); } ); """ * **Don't Do This:** """javascript import { atom, useAtom } from 'jotai' import { useEffect, useState } from 'react' const basePriceAtom = atom(10) const discountAtom = atom(0.2) function PriceDisplay() { const [basePrice] = useAtom(basePriceAtom) const [discount] = useAtom(discountAtom) const [discountedPrice, setDiscountedPrice] = useState(0) useEffect(() => { setDiscountedPrice(basePrice * (1 - discount)) }, [basePrice, discount]) return <p>Discounted Price: {discountedPrice}</p> } """ ### 3.3. Atom Scope * **Standard:** Leverage "jotai-scope" when working with multiple instances of the same component, each requiring isolated state. Avoid global atoms for component-specific state. * **Why:** Prevents state collisions and ensures that each component instance maintains its own independent state. * **Do This:** """javascript // Using jotai-scope within a component import { useScopedAtom } from 'jotai-scope' import { atom } from 'jotai' const countAtom = atom(0) function Counter() { const [count, setCount] = useScopedAtom(countAtom) //Using scoped atom return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ) } """ * **Don't Do This:** Use global atoms when the state should be component-specific, which will lead to unexpected behavior in multi-instance scenarios. ### 3.4. Atom Updates * **Standard:** Prefer functional updates when updating atoms based on their previous value. * **Why:** Avoids issues with stale state, especially when updates are batched. Guarantees that you are operating on the most recent value of the atom. * **Do This:** """javascript import { atom, useAtom } from 'jotai' const countAtom = atom(0) function Counter() { const [count, setCount] = useAtom(countAtom) const increment = () => { setCount((prevCount) => prevCount + 1) // functional update } return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ) } """ * **Don't Do This:** """javascript import { atom, useAtom } from 'jotai' const countAtom = atom(0) function Counter() { const [count, setCount] = useAtom(countAtom) const increment = () => { setCount(count + 1) // Direct update - prone to stale state issues } return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ) } """ ### 3.5. Asynchronous Operations * **Standard:** When dealing with asynchronous operations, create asynchronous derived atoms using Jotai's "atom" function. Handle loading and error states explicitly. * **Why:** Allows centralized management of async states. * **Do This:** """javascript import { atom } from 'jotai' const apiDataAtom = atom(async () => { try { const response = await fetch('/api/data') const data = await response.json() return data } catch (error) { console.error('Error fetching data:', error) throw error // Re-throw the error to be caught by the component } }) const dataAtom = atom( async (get) => { return await get(apiDataAtom) } ); """ """javascript import { useAtom } from 'jotai' function DataComponent() { const [data] = useAtom(dataAtom) if (!data) { return <p>Loading...</p> } return <p>Data: {data.value}</p> } """ * **Don't Do This:** Directly call asynchronous functions within components using "useEffect" and manage state with "useState". Centralize the asynchronous operations and loading states within the atoms. ### 3.6. Selectors (Computed Values) * **Standard:** Use Jotai's derived atoms as selectors to compute values from other atoms. This provides a centralized way to access a computed value. * **Why:** Avoids redundant computations across the codebase. Allows for easy caching and memoization by Jotai. * **Do This:** """javascript import { atom } from 'jotai' const todosAtom = atom([ { id: 1, text: 'Learn Jotai', completed: true }, { id: 2, text: 'Build awesome apps', completed: false }, ]) const completedTodosAtom = atom((get) => { const todos = get(todosAtom) return todos.filter((todo) => todo.completed) }) """ """javascript import { useAtom } from 'jotai' import { completedTodosAtom } from './atoms' function CompletedTodosList() { const [completedTodos] = useAtom(completedTodosAtom) return ( <ul> {completedTodos.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ) } """ * **Don't Do This:** Recompute the same derived value in multiple components. ## 4. TypeScript Considerations Using TypeScript with Jotai provides type safety and improves code maintainability. ### 4.1. Type Annotations * **Standard:** Provide explicit type annotations for atoms, especially when the initial value doesn't provide enough information for type inference. * **Why:** Ensures type safety and prevents unexpected type-related errors. * **Do This:** """typescript import { atom } from 'jotai' interface User { id: number name: string } const userAtom = atom<User | null>(null) // Explicit type annotation const countAtom = atom<number>(0) """ * **Don't Do This:** """typescript import { atom } from 'jotai' const userAtom = atom(null) // Implicit type - could be anything """ ### 4.2. Custom Atoms * **Standard:** Define types for custom atoms that involve complex logic or transformations. * **Why:** Improves code clarity and provides better type checking. * **Do This:** """typescript import { atom } from 'jotai' interface Todo { id: number text: string completed: boolean } const todosAtom = atom<Todo[]>([]) const addTodoAtom = atom( (get) => get(todosAtom), (get, set, newTodo: Omit<Todo, 'id'>) => { const todos = get(todosAtom) const nextId = todos.length > 0 ? Math.max(...todos.map(todo => todo.id)) + 1 : 1; set(todosAtom, [...todos, { id: nextId, ...newTodo }]) } ) """ ### 4.3 Generics * **Standard:** Use generics when creating reusable utility functions or components that interact with Jotai atoms, making them type-safe. * **Why:** Provides flexibility and type safety when working with different types of atoms. * **Do This:** """typescript import { atom, useAtom } from 'jotai'; import { useState, useEffect } from 'react'; function useAtomWithLocalStorage<T>(key: string, initialValue: T) { const localStorageValue = localStorage.getItem(key); const initial = localStorageValue ? JSON.parse(localStorageValue) : initialValue; const myAtom = atom<T>(initial); const [value, setValue] = useAtom(myAtom); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue] as const; } """ ## 5. Performance Optimization Jotai is designed for performance, but here are some ways to maximize it. ### 5.1. Minimize Atom Updates * **Standard:** Avoid unnecessary atom updates. Only update atoms when the underlying data has actually changed. * **Why:** Reduces unnecessary re-renders and improves application performance. * **Do This:** Use referential equality checks before updating atoms, especially when dealing with complex objects or arrays. Immer can also be used (though it adds overhead and complexity). * **Don't Do This:** Update atoms on every render, even if the data hasn't changed. ### 5.2. Optimize Derived Atoms * **Standard:** Use Jotai's built-in memoization for derived atoms. Be mindful of the dependencies of derived atoms, as changes to any dependency will trigger a re-computation. * **Why:** Avoids unnecessary re-computations and improves performance. * **Do This:** Keep the logic inside derived atoms as simple and efficient as possible. Complex calculations *may* benefit from manual memoization *inside* the atom, but test that first. * **Don't Do This:** Perform heavy computations directly within components. ### 5.3. Batch Updates * **Standard:** Prefer "useUpdateAtom" from jotai/utils when updating atoms in a loop or rapidly in succession to avoid triggering too many re-renders. * **Why:** Improves performance when multiple atom update triggers render optimizations. * **Do this:** Use "useUpdateAtom" inside the loop as demonstrated below. """javascript import { atom, useSetAtom } from 'jotai' const countAtom = atom(0) function Counter() { const setCount = useSetAtom(countAtom) const incrementMany = () => { for (let i = 0; i < 100; i++) { setCount((prevCount) => prevCount + 1); } } return ( <div> <button onClick={incrementMany}>Increment 100</button> </div> ) } """ * **Don't do this:** Triggering lots of updates at once to "useAtom" when you can batch them. ## 6. Security Considerations While Jotai itself doesn't introduce significant security vulnerabilities, it's crucial to follow general security best practices when handling data within atoms. ### 6.1. Sensitive Data * **Standard:** Avoid storing sensitive information (e.g., passwords, API keys, personal identifiable information) directly in Jotai atoms, especially on the client-side. * **Why:** Prevents exposure of sensitive data through browser history, debugging tools, or compromised environments. * **Do This:** Handle sensitive data on the server-side and only store non-sensitive representations of the data in Jotai atoms (e.g., user ID instead of password). * **Don't Do This:** Store passwords or API keys in client-side atoms. ### 6.2. Input Validation * **Standard:** Validate all user inputs before updating atoms. * **Why:** Prevents injection attacks and ensures data integrity. * **Do This:** Use appropriate validation techniques based on the type of data (e.g., regular expressions, type checking, server-side validation). * **Don't Do This:** Directly update atoms with unvalidated user inputs. ### 6.3. Sanitization * **Standard:** Sanitize data retrieved from atoms before rendering it in the UI to prevent cross-site scripting (XSS) attacks. * **Why:** Prevents malicious code injection. * **Do This:** Use a library like "DOMPurify" to sanitize HTML content before rendering it. * **Don't Do This:** Directly render unsanitized data from atoms in the UI. By adhering to these code style and convention standards, development teams can ensure code quality, maintainability, and efficient collaboration when building applications with Jotai. Remember that these guidelines are intended to promote best practices but can be adapted to suit specific project requirements and team preferences. These standards, if adopted, directly enhance the developer experience when integrating with AI driven tools such as Copilot and / or Cursor.
# Testing Methodologies Standards for Jotai This document outlines the standards for testing Jotai state management in React applications, considering best practices, performance, and maintainability. ## Unit Testing for Jotai Atoms Unit testing focuses on individual atoms and their derived values in isolation. This ensures that each atom behaves as expected given specific inputs or conditions. ### Standards * **Do This**: Test atom initial values. * **Do This**: Test atom value updates using "setAtom". * **Do This**: Test derived atoms' value calculations based on their dependencies using "get". * **Don't Do This**: Avoid testing implementation details of Jotai itself; focus on your atom's logic. ### Why These Standards Matter * **Maintainability**: Isolated tests verify that each atom's logic remains correct as the codebase evolves. * **Performance**: Identifying performance issues early in individual atoms prevents cascading performance problems. * **Correctness**: Ensures that atoms behave predictably under different conditions and inputs. ### Code Examples #### Testing a simple atom """javascript import { atom } from 'jotai'; import { renderHook, act } from '@testing-library/react-hooks'; describe('Simple Atom', () => { const countAtom = atom(0); it('should initialize with the correct value', () => { const { result } = renderHook(() => countAtom.readOnly); expect(result.current).toBe(0); }); it('should update the value correctly', () => { const { result } = renderHook(() => { const [count, setCount] = useAtom(countAtom); return { count, setCount }; }); act(() => { result.current.setCount(10); }); expect(result.current.count).toBe(10); }); }); """ **Explanation:** * "renderHook" from "@testing-library/react-hooks" allows you to test React hooks, which Jotai atoms often use. * "useAtom" is used within "renderHook", providing the atom’s current value and a setter function. * "act" wraps any state updates to ensure React's batching of updates is handled correctly during testing. * The initial value and subsequent updates of the atom are explicitly asserted. #### Testing a derived atom """javascript import { atom } from 'jotai'; import { renderHook } from '@testing-library/react-hooks'; describe('Derived Atom', () => { const priceAtom = atom(10); const discountAtom = atom(0.2); const discountedPriceAtom = atom((get) => { const price = get(priceAtom); const discount = get(discountAtom); return price * (1 - discount); }); it('should calculate the derived value correctly', () => { const { result } = renderHook(() => useAtomValue(discountedPriceAtom)); expect(result.current).toBe(8); }); it('should update the derived value when dependencies change', () => { const { result, rerender } = renderHook(() => { const [discountedPrice] = useAtom(discountedPriceAtom); return discountedPrice; }); const newPrice = atom(20); const newDiscountedPriceAtom = atom((get) => { const price = get(newPrice); const discount = get(discountAtom); return price * (1 - discount); }); const { result: newResult } = renderHook(() => useAtomValue(newDiscountedPriceAtom)); act(() => { newPrice.write(20); }); expect(newResult.current).toBe(16); }); }); """ **Explanation:** * This example tests an atom that derives its value from other atoms. * The test verifies that the derived value is computed correctly and that it updates when the dependencies change. Explicit dependency updates might require creating new atom instances if using vanilla Jotai atom definitions directly, which is shown as simulating a dependency change. Normally, changing atom values will propagate updates in a normal Jotai setup when using "useAtom" or similar hooks. ### Common Anti-Patterns * **Over-reliance on mocking Jotai internals**: Mocking "atom" or "useAtom" directly can lead to brittle tests that break when Jotai is updated. Test the behavior of your atoms, not the Jotai library. * **Ignoring edge cases**: Ensure that your tests cover edge cases and boundary conditions for your atom's logic. ## Integration Testing with Jotai Integration testing examines how Jotai atoms interact with React components and other parts of the application. This verifies that the state management integrates seamlessly with the UI and business logic. ### Standards * **Do This**: Test how components render based on different atom states. * **Do This**: Verify that user interactions correctly update atom values. * **Do This**: Use testing libraries like React Testing Library to simulate user behavior. * **Don't Do This**: Avoid shallow rendering components in integration tests unless absolutely necessary. Focus on testing the full component rendering and behavior. ### Why These Standards Matter * **Correctness**: Ensure atoms and components interact correctly to deliver the expected application behavior. * **UI Integrity**: Verify that state changes are reflected accurately in the user interface. * **Realistic Scenarios**: Integration tests mimic real user interactions, providing higher confidence in the application's functionality. ### Code Examples """javascript import { atom, useAtom } from 'jotai'; import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; const countAtom = atom(0); function CounterComponent() { const [count, setCount] = useAtom(countAtom); return ( <div> <span>Count: {count}</span> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } describe('Counter Component Integration', () => { it('should render the initial count', () => { render(<CounterComponent />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('should increment the count when the button is clicked', () => { render(<CounterComponent />); const incrementButton = screen.getByText('Increment'); fireEvent.click(incrementButton); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); }); """ **Explanation:** * This test renders a "CounterComponent" that uses a Jotai atom. * "React Testing Library" is used to find elements within the component and simulate user interactions. * The test verifies that clicking the "Increment" button updates the atom's value, which is then reflected in the component's rendered output. ### Common Anti-Patterns * **Ignoring asynchronous updates**: If an atom update triggers an asynchronous operation (e.g., fetching data), ensure your tests wait for the operation to complete before asserting the final state using "async/await" and appropriate waiting mechanisms from React Testing Library ("waitFor", "findByText", etc.). * **Testing implementation details of components**: Focus on testing the component's behavior from a user's perspective, rather than testing internal implementation details that could change without affecting the user experience. ## End-to-End (E2E) Testing End-to-end tests validate the entire application flow, from the user interface to the backend, ensuring that all parts work together correctly. This generally uses tools like Cypress or Playwright. ### Standards * **Do This**: Simulate complete user workflows, such as logging in, navigating pages, and submitting forms. * **Do This**: Verify data persistence and consistency across multiple sessions. * **Do This**: Use E2E testing frameworks like Cypress or Playwright for browser automation. * **Don't Do This**: Rely solely on E2E tests. They are slower and more brittle than unit or integration tests. Use them to complement other types of tests. ### Why These Standards Matter * **Full System Validation**: E2E tests ensure that the entire application works as expected, including interactions with external services. * **Regression Detection**: Identifying regressions early in the development cycle prevents issues from reaching users. * **Confidence**: Passing E2E tests provides high confidence in the application's overall stability and functionality. ### Code Examples (Conceptual) **Assuming Cypress for E2E Testing:** """javascript // cypress/e2e/counter.cy.js describe('Counter Workflow', () => { it('should increment the counter through the UI', () => { cy.visit('/'); // Visit the homepage // Verify initial state cy.get('[data-testid="count"]').should('contain', '0'); // Click the increment button cy.get('[data-testid="increment-button"]').click(); // Verify the counter has incremented cy.get('[data-testid="count"]').should('contain', '1'); // Refresh the page cy.reload(); // Verify the count is persisted (assuming localStorage or similar) cy.get('[data-testid="count"]').should('contain', '1'); }); }); """ **Explanation:** * This example uses Cypress to automate a test scenario in a real browser environment. * The test visits the application's homepage, verifies the initial counter value, clicks the increment button, and verifies that the counter updates correctly. * A reload is triggered to check for data persistence, demonstrating an E2E test's ability to cover broader system aspects. * The actual code assumes suitable "data-testid" attributes exist on the elements. ### Common Anti-Patterns * **Unreliable test selectors**: Using CSS selectors that are prone to change can lead to flaky tests. Use more robust selectors like "data-testid" attributes. * **Insufficient waiting**: Failing to wait for elements to appear or for asynchronous operations to complete can cause tests to fail intermittently. Use Cypress's built-in waiting mechanisms or explicit waits when necessary. * **Testing too much in a single E2E test**: Keep E2E tests focused on specific user workflows to make them easier to debug and maintain. ## Testing Jotai Asynchronous Atoms Asynchronous atoms involve fetching data or performing other asynchronous operations. Proper testing is crucial to ensure data consistency and handle loading states. ### Standards * **Do This**: Test loading states using mocking libraries or test doubles, for appropriate state updates while fetching data (e.g. "pending" -> "success" -> "error"). * **Do This**: Verify correct data transformation and error handling when the asynchronous operation completes. * **Don't Do This**: Assume asynchronous operations complete instantly in tests. Use "async/await" and appropriate waiting mechanisms to handle the asynchronous nature of these operations. ### Why These Standards Matter * **Data Integrity**: Ensures that the application handles asynchronous data correctly, preventing data inconsistencies and errors. * **User Experience**: Testing loading states guarantees a smooth user experience while data is being fetched. * **Resilience**: Verifies that the application is resilient to network errors and other asynchronous issues. ### Code Examples """javascript import { atom, useAtom } from 'jotai'; import { renderHook, act } from '@testing-library/react-hooks'; // Simulate an asynchronous data fetch const fetchData = async () => { return new Promise(resolve => { setTimeout(() => { resolve({ data: 'Async Data' }); }, 50); // Simulate a small delay }); }; describe('Asynchronous Atom', () => { const asyncDataAtom = atom(async (get) => { try { const result = await fetchData(); return { status: 'success', data: result.data }; } catch (error) { return { status: 'error', error: error.message }; } }); it('should handle the asynchronous data correctly', async () => { const { result, waitForNextUpdate } = renderHook(() => useAtomValue(asyncDataAtom)); // Wait for the async operation to complete await waitForNextUpdate(); // Assert success state expect(result.current.status).toBe('success'); expect(result.current.data).toBe('Async Data'); }); it('should handle errors correctly', async() => { // Mock fetchData to reject const rejectedData = async () => Promise.reject(new Error("Failed")); const errorAtom = atom(async (get) => { try { const result = await rejectedData(); return { status: 'success', data: result.data }; } catch (error) { return { status: 'error', error: error.message }; } }); const { result, waitForNextUpdate } = renderHook(() => useAtomValue(errorAtom)); await waitForNextUpdate(); expect(result.current.status).toBe('error'); expect(result.current.error).toBe("Failed") }) }); """ **Explanation:** * This example tests an atom that performs an asynchronous data fetch and manages loading states. * "fetchData" simulates an API call with a delay. * The test waits for the asynchronous operation to complete using "waitForNextUpdate" and uses "async/await". * It validates the success state and extracted data after fetching completion. * Error handling is tested through a mocked, failing data fetching function. ### Common Anti-Patterns * **Not handling loading states**: Forgetting to test the loading state can result in a poor user experience while data loads in the background. * **Using real API endpoints in tests**: Using real API endpoints in tests can make tests slow and unreliable. Use mocking libraries like "msw" (Mock Service Worker) to mock API responses. ## Performance Testing Jotai Atoms Performance testing is essential for ensuring Jotai atoms don't become a bottleneck in your application, especially with complex derived atoms or frequent state updates. ### Standards * **Do This**: Measure the time it takes to calculate derived atom values, especially for complex calculations. * **Do This**: Monitor re-renders of components that depend on frequently updated atoms. * **Do This**: Use memoization techniques (e.g., "useMemo", "useCallback") to optimize derived atom calculations and prevent unnecessary re-renders. * **Don't Do This**: Optimize prematurely. Measure performance first and then identify the most significant bottlenecks. ### Why These Standards Matter * **Responsiveness**: Ensures the UI remains responsive even with complex state management. * **Efficiency**: Reduces unnecessary CPU usage and memory allocation. * **Scalability**: Helps identify potential performance issues as the application grows in complexity. ### Code Examples """javascript import { atom, useAtomValue } from 'jotai'; import { renderHook } from '@testing-library/react-hooks'; describe('Performance Testing', () => { it('should measure the calculation time of a derived atom', () => { const baseAtom = atom(0); const expensiveDerivedAtom = atom((get) => { const start = performance.now(); let result = get(baseAtom); // Simulate a computationally expensive operation for (let i = 0; i < 1000000; i++) { result += i; } const end = performance.now(); return { result, time: end - start }; }); const { result } = renderHook(() => useAtomValue(expensiveDerivedAtom)); console.log("Calculation time: ${result.current.time} ms"); // Assert that the calculation time is within acceptable limits expect(result.current.time).toBeLessThan(500); // Example threshold }); it('should measure component re-renders', () => { const renderCounter = jest.fn(); function MyComponent () { //A functional react component renderCounter() return (<div>Test</div>) } renderCounter(); expect(renderCounter).toHaveBeenCalledTimes(1) // Check the number of calls in the mock }) }); """ **Explanation:** * This example shows how to profile the calculation time of a derived atom using "performance.now()". * A long running for loop is used as a heavy task, that is timed within a get function on an atom * The test measures the time it takes to perform a computationally intensive calculation. * The assertion can be updated to check different amounts of calls to the functional component ### Common Anti-Patterns * **Ignoring performance in tests**: Focusing solely on functional correctness and neglecting performance testing can lead to performance regressions. * **Not profiling in realistic conditions**: Profiling in a test environment that does not accurately simulate the production environment (e.g., data volume, network conditions) can provide misleading results. * **Over-optimizing without measuring**: Attempting to optimize performance without first measuring and identifying bottlenecks can waste time and potentially make the code less readable and maintainable. These standards provide a comprehensive approach to testing Jotai state management, encompassing unit, integration, end-to-end, and performance testing aspects. Following these guidelines will help ensure the reliability, maintainability, and performance of your Jotai-based React applications.