# API Integration Standards for Remix
This document outlines the coding standards for API integration in Remix applications. These standards aim to promote maintainability, performance, security, and a consistent development experience. They are tailored for the latest version of Remix and incorporate modern best practices.
## 1. Architectural Principles
### 1.1. Decoupling Frontend and Backend
**Standard:** Adhere to a clear separation of concerns between the Remix frontend and the backend API. The frontend should primarily handle UI rendering, user interactions, and calling backend APIs. The backend should focus on data management, business logic, and security.
**Why:** Decoupling enhances maintainability, testability, and scalability. It allows different teams to work on the frontend and backend independently, reducing the risk of conflicts and improving development velocity.
**Do This:**
* Define clear API contracts (e.g., using OpenAPI/Swagger) to specify data formats and endpoints.
* Use environment variables to configure API base URLs in different environments.
* Implement a thin API client layer in the Remix app to handle API calls.
**Don't Do This:**
* Embed complex business logic directly within Remix components.
* Hardcode API URLs or credentials in the frontend code.
* Directly access databases from the frontend.
**Example:**
"""typescript
// app/utils/api-client.ts
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
export async function fetchProducts() {
const response = await fetch("${API_BASE_URL}/products");
if (!response.ok) {
throw new Error("Failed to fetch products: ${response.status}");
}
return await response.json();
}
export async function createProduct(productData: any) {
const response = await fetch("${API_BASE_URL}/products", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(productData),
});
if (!response.ok) {
throw new Error("Failed to create product: ${response.status}");
}
return await response.json();
}
"""
"""typescript
// app/routes/products.tsx
import { useLoaderData } from "@remix-run/react";
import { fetchProducts } from "~/utils/api-client";
export const loader = async () => {
const products = await fetchProducts();
return products
}
export default function Products() {
const products = useLoaderData();
return (
{products.map((product: any) => (
{product.name}
))}
);
}
"""
### 1.2. API Gateway Pattern
**Standard:** Consider using an API Gateway as a single entry point for all backend services, especially in microservices architectures.
**Why:** An API Gateway simplifies client interactions, handles authentication/authorization, rate limiting, and other cross-cutting concerns. It also allows you to evolve backend services without impacting the frontend.
**Do This:**
* Configure the API Gateway to route requests to the appropriate backend services based on URL paths.
* Implement authentication and authorization at the API Gateway level.
* Use the API Gateway for rate limiting and request throttling.
**Don't Do This:**
* Expose backend services directly to the frontend.
* Duplicate cross-cutting concerns in each backend service.
### 1.3. Backend-For-Frontend (BFF) Pattern (Remix Specific)
**Standard:** When the needs of different frontends diverge significantly, consider implementing a Backend-For-Frontend (BFF) layer specifically tailored for Remix. This is especially powerful as Remix applications often encompass both server-side data fetching *and* UI rendering.
**Why:** The BFF pattern allows tailoring API responses and interactions based on the specific needs of Remix, potentially optimizing data fetching efficiency and simplifying client-side logic. This reduces the over-fetching or under-fetching of data
**Do This:**
* Create a dedicated BFF layer written in Node.js, or similar, that acts as a layer between the Remix frontend and the core backend services.
* Implement data aggregation and transformation in the BFF to provide the Remix frontend with exactly the data it needs.
* Tailor the BFF's API endpoints to match the specific Remix route loaders and actions.
**Don't Do This:**
* Rely on a generic backend API that caters to all clients when Remix needs specific data transformations or aggregations.
* Overload the Remix route loaders with complex data manipulation logic that belongs in a dedicated BFF layer
**Example:** Imagine a core "products" service that returns a large amount of data. The Remix frontend only needs the product name and price for a listing page, but needs full detail when viewing a single product.
"""typescript
// BFF - api/bff/products.ts (Example using Express.js)
import express from 'express';
const app = express();
const productsServiceURL = process.env.PRODUCTS_API_URL || 'http://localhost:3002'; // URL of your core product service
app.get('/listing', async (req, res) => {
try {
const response = await fetch("${productsServiceURL}/products");
const products = await response.json();
// Only return name and price
const listingData = products.map((p: any) => ({ id: p.id, name: p.name, price: p.price }));
res.json(listingData);
} catch (error) {
console.error("Error fetching products for listing:", error);
res.status(500).json({ error: 'Failed to fetch listing data' });
}
});
app.get('/:id', async (req, res) => {
try {
const response = await fetch("${productsServiceURL}/products/${req.params.id}");
const product = await response.json();
res.json(product); // Return all product details
} catch (error) {
console.error("Error at BFF fetching product details:", error);
res.status(500).json({ error: 'Failed to fetch product details' });
}
});
app.listen(3003, () => {
console.log('BFF listening on port 3003');
});
export default app;
"""
"""typescript
// Remix - app/routes/products/listing.tsx
import { useLoaderData } from "@remix-run/react";
const BFF_URL = process.env.BFF_URL || 'http://localhost:3003'; // URL of your BFF
export const loader = async () => {
const response = await fetch("${BFF_URL}/products/listing"); //BFF endpoint
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
return await response.json();
};
export default function ProductListing() {
const products = useLoaderData();
return (
{products.map((product: any) => (
{product.name} - ${product.price}
))}
);
}
"""
## 2. Data Fetching Strategies
### 2.1. Leveraging Remix Loaders and Actions
**Standard:** Use Remix loaders for fetching data on the server and actions for server-side data mutations. This allows the framework to handle caching, prefetching, and optimistic UI updates efficiently.
**Why:** Loaders and actions are fundamental to Remix and using them correctly improves performance. Loaders allow your application to fetch data from a server *before* rendering the UI and data mutations should use actions.
**Do This:**
* Fetch data in loaders and pass it to the component via "useLoaderData".
* Use actions for form submissions and data modifications. Return data from actions that can trigger revalidation or direct passing.
* Utilize Remix's "useFetcher" hook for data fetching in components outside the main route hierarchy (e.g., in modals or sidebars).
**Don't Do This:**
* Use "useEffect" with "useState" to manage asynchronous data fetching in components, as this bypasses Remix's built-in data handling mechanisms.
* Perform data mutations directly in loaders.
* Bypass the Remix's data fetching by calling APIs directly in components using "fetch" or similar libraries while ignoring Remix's "loader" and "action" functions.
**Example:**
"""typescript
// app/routes/todos.tsx
import { json, redirect } from "@remix-run/node";
import {
useLoaderData,
useTransition,
Form,
useActionData,
} from "@remix-run/react";
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
export const loader = async () => {
const response = await fetch("${API_BASE_URL}/todos");
if (!response.ok) {
throw new Error("Failed to fetch todos: ${response.status}");
}
return json(await response.json());
};
export const action = async ({ request }: { request: Request }) => {
const formData = await request.formData();
const text = formData.get("text") as string;
const response = await fetch("${API_BASE_URL}/todos", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text, completed: false }),
});
if (!response.ok) {
return json({errors: {form: "Unable to create todo"}}, {status:400});
}
return redirect("/todos");
};
export default function TodosRoute() {
const todos = useLoaderData();
const actionData = useActionData();
return (
Todos
{todos.map((todo: any) => (
{todo.text}
))}
Add Todo
{actionData?.errors?.form && (
<p>{actionData.errors.form}</p>
)}
);
}
"""
### 2.2. Caching Strategies
**Standard:** Implement appropriate caching strategies to minimize API requests and improve application performance.
**Why:** Caching reduces latency, improves responsiveness, and reduces the load on backend services.
**Do This:**
* Leverage Remix's built-in caching mechanisms (e.g., "cache-control" headers) to cache API responses in the browser.
* Consider using a server-side caching layer (e.g., Redis) to cache frequently accessed data.
* Implement stale-while-revalidate (SWR) or similar strategies to serve cached data immediately while updating it in the background.
**Don't Do This:**
* Disable caching altogether.
* Cache sensitive data without proper security measures.
* Use excessively long cache durations without considering data staleness.
**Example:**
"""typescript
// app/routes/products.tsx
import { json } from "@remix-run/node";
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
export const loader = async () => {
const response = await fetch("${API_BASE_URL}/products", {
headers: {
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
},
});
if (!response.ok) {
throw new Error("Failed to fetch products: ${response.status}");
}
return json(await response.json(), {
headers: {
"Cache-Control": "public, max-age=3600, s-maxage=3600", // Browser and CDN cache
},
});
};
"""
### 2.3. Optimistic Updates
**Standard:** Implement optimistic updates in actions to provide a responsive user experience by predicting the outcome of a mutation and updating the UI immediately, before the server confirms the change.
**Why:** Optimistic updates create a perceived performance boost, making the application feel faster, especially for slower connections.
**Do This:**
* Update the UI optimistically in the action data immediately. If the server returns an error, revert the update.
* Display a loading state or spinner while waiting for the server response.
* Handle errors gracefully and revert the optimistic update if the server rejects the change.
**Don't Do This:**
* Implement optimistic updates without proper error handling and rollback mechanisms.
* Use optimistic updates for critical operations where data integrity is paramount.
**Example:**
"""typescript
// app/routes/delete-product.tsx
import { redirect } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { useEffect } from "react";
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
export async function action({ request }: { request: Request }) {
const formData = await request.formData();
const productId = formData.get("productId");
try {
const response = await fetch("${API_BASE_URL}/products/${productId}", {
method: "DELETE",
});
if (!response.ok) {
//Revalidate
throw new Error("Failed to delete product: ${response.status}");
}
return redirect("/products"); // Optimistic redirect
}
catch (error: any) {
console.error("Error deleting product:", error);
return redirect("/products"); //Revalidate
}
}
export default function DeleteProduct() {
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.state === "done" && fetcher.type === "POST") {
// Redirect user after delation
}
}, [fetcher.state, fetcher.type]);
return (
{fetcher.state === "submitting" ? "Deleting..." : "Delete Product"}
);
}
"""
## 3. API Client Implementation
### 3.1. Centralized API Client
**Standard:** Create a centralized API client module that handles API requests and responses.
**Why:** A centralized API client promotes code reuse, simplifies API versioning, and facilitates error handling and authentication.
**Do This:**
* Encapsulate API base URLs, headers, and authentication logic in the API client.
* Provide utility functions for common API operations (e.g., "get", "post", "put", "delete").
* Implement error handling and retry mechanisms in the API client.
**Don't Do This:**
* Duplicate API request logic throughout the codebase.
* Expose raw "fetch" calls directly in components.
### 3.2. Data Serialization and Deserialization
**Standard:** Use consistent data serialization and deserialization strategies to ensure data integrity and compatibility between the frontend and backend.
**Why:** Consistent data handling prevents data corruption, simplifies debugging, and improves interoperability.
**Do This:**
* Use JSON for API requests and responses.
* Implement data validation on both the frontend and backend.
* Use a library like "zod" (or similar tools) to define schemas and validate data.
**Don't Do This:**
* Use inconsistent data formats or encodings.
* Rely solely on backend validation without frontend validation.
"""typescript
// app/utils/api-client.ts
import { z } from "zod";
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(3),
description: z.string().optional(),
price: z.number().positive(),
});
type Product = z.infer;
export async function fetchProducts(): Promise {
const response = await fetch("${API_BASE_URL}/products");
if (!response.ok) {
throw new Error("Failed to fetch products: ${response.status}");
}
const data = await response.json();
return z.array(ProductSchema).parse(data); // Validation
}
export async function createProduct(productData: Omit): Promise {
const parsedData = ProductSchema.omit({ id: true }).parse(productData); // Validation
const response = await fetch("${API_BASE_URL}/products", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(parsedData),
});
if (!response.ok) {
throw new Error("Failed to create product: ${response.status}");
}
const newProduct = await response.json();
return ProductSchema.parse(newProduct); // Validate the returned product too
}
// Example usage in a route
"""
### 3.3. Error Handling
**Standard:** Implement robust error handling mechanisms to gracefully handle API errors and provide informative feedback to the user.
**Why:** Proper error handling prevents application crashes, improves user experience, and simplifies debugging.
**Do This:**
* Use "try...catch" blocks to handle potential API errors.
* Display user-friendly error messages.
* Log errors to a central error tracking system (e.g., Sentry).
* Implement retry mechanisms for transient errors.
**Don't Do This:**
* Ignore API errors.
* Display raw error messages to the user.
* Expose sensitive information in error messages.
## 4. Security Considerations
### 4.1. Authentication and Authorization
**Standard:** Implement secure authentication and authorization mechanisms to protect API endpoints from unauthorized access.
**Why:** Authentication verifies the identity of the user, while authorization determines their access rights.
**Do This:**
* Use industry-standard authentication protocols (e.g., OAuth 2.0, JWT).
* Store passwords securely using hashing and salting.
* Implement role-based access control (RBAC) to restrict access to sensitive data and operations.
* Validate API requests to prevent injection attacks.
**Don't Do This:**
* Store passwords in plain text.
* Grant excessive permissions to users.
* Expose sensitive data in public API endpoints.
### 4.2. Data Validation and Sanitization
**Standard:** Validate and sanitize all input data to prevent injection attacks (e.g., SQL injection, XSS).
**Why:** Data validation ensures that the data conforms to the expected format and constraints, while sanitization removes or escapes potentially malicious characters.
**Do This:**
* Use server-side validation to verify that all input data is valid.
* Sanitize user-generated content to prevent XSS attacks.
* Use parameterized queries to prevent SQL injection attacks.
**Don't Do This:**
* Trust user input without validation.
* Concatenate user input directly into SQL queries.
* Disable XSS protection.
### 4.3. Rate Limiting and Throttling
**Standard:** Implement rate limiting and throttling to protect API endpoints from abuse and prevent denial-of-service (DoS) attacks.
**Why:** Rate limiting restricts the number of requests that a user can make within a given time period, while throttling slows down or rejects requests that exceed a certain threshold.
**Do This:**
* Implement rate limiting at the API Gateway level.
* Return appropriate HTTP status codes (e.g., 429 Too Many Requests) when rate limits are exceeded.
* Use adaptive rate limiting techniques to adjust rate limits based on traffic patterns.
**Don't Do This:**
* Expose API endpoints without rate limiting.
* Use overly restrictive rate limits that impact legitimate users.
## 5. Technology-Specific Considerations
### 5.1. Remix and Serverless Functions
**Standard:** When using Remix with serverless functions (e.g., AWS Lambda, Netlify Functions), ensure that API integrations are optimized for serverless environments.
**Why:** Serverless functions have limitations on execution time, memory, and storage. Optimizing API interactions is crucial for performance and cost efficiency.
**Do This:**
* Minimize the size of serverless function bundle.
* Use connection pooling to reuse database connections.
* Cache frequently accessed data in serverless function memory or in a shared caching layer.
**Don't Do This:**
* Perform long-running operations in serverless functions.
* Store large amounts of data in serverless function memory.
### 5.2. Managing Environment Variables
**Standard:** Consistently use environment variables for managing API keys, secrets, and configuration settings.
**Why:** Environment variables keep sensitive information out of code, making deployments more manageable and secure.
**Do This:**
* Store API keys, database credentials, and other sensitive values as environment variables.
* Use a secure method for managing environment variables, especially in production (e.g., Doppler, HashiCorp Vault).
* Use ".env" files for local development, but ensure they are not committed to source control.
**Don't Do This:**
* Hardcode API keys or secrets directly in your code.
* Commit ".env" files containing sensitive information to your repository.
This document provides a comprehensive set of standards for API integration in Remix. By adhering to these standards, developers can build robust, maintainable, and secure Remix applications that deliver a superior user experience.
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'
# Testing Methodologies Standards for Remix This document outlines the testing methodologies standards for Remix applications. Adhering to these standards will result in more maintainable, reliable, and performant Remix applications. These standards are designed for the latest version of Remix and promote modern testing practices. ## 1. General Testing Philosophy Testing is a crucial part of the development lifecycle. In Remix, due to its server-side and client-side code integration, a multi-faceted testing approach is essential. Unit tests focus on individual modules, integration tests ensure components work together seamlessly, and end-to-end tests validate the application flow from the user's perspective. ### 1.1. Test Types and Coverage * **Unit Tests:** Aim for high coverage (80%+) for core application logic, especially within utility functions, models, and formatters. These should be fast and isolated. * **Integration Tests:** Focus on testing the interactions between different parts of your application, such as components interacting with data loaders or actions. * **End-to-End (E2E) Tests:** E2E tests should cover critical user flows and ensure the entire application is working as expected. Focus on high-impact user journeys. **Do This:** * Prioritize testing business logic and critical data flows. * Use code coverage tools to identify areas with insufficient test coverage. * Maintain a balance between different test types. **Don't Do This:** * Treat code coverage as the only measure of testing quality. * Write E2E tests for everything. They are slower and more brittle. * Skip testing "simple" functions. Bugs often hide in seemingly simple code. ### 1.2. Testing Pyramid Follow the testing pyramid principle, emphasizing unit tests, then integration tests, and finally end-to-end tests. This strategy helps maintain a fast and efficient testing process. **Why:** A large number of unit tests provides rapid feedback and pinpoints issues quickly. E2E tests, while valuable, are slower and more expensive to run and maintain. ## 2. Unit Testing Unit tests verify the functionality of individual functions, classes, or modules in isolation. ### 2.1. Tools and Libraries * **Jest:** (Recommended) Popular JavaScript testing framework with built-in support for mocking, spying, and coverage. Is simple to set up and has first class support. * **Vitest:** An alternative to Jest with a focus on speed and compatibility with Vite. Useful if you’re already using Vite in your project. * **Testing Library:** (Recommended) A set of utilities that make it easy to test React components in a user-centric way. Its focus is on testing what the user sees and interacts with. * **msw (Mock Service Worker):** Intercepts network requests at the browser level, mocking API responses for isolated component testing. Integrates well with Testing Library. * **Sinon.js:** A standalone test spies, stubs and mocks library for JavaScript. Useful if you need more control over mocking than what Jest provides. ### 2.2. Best Practices * **Isolate Units:** Mock dependencies to isolate the unit under test. * **Clear Assertions:** Write clear and specific assertions that describe the expected behavior. * **Test Driven Development (TDD):** Consider writing tests before implementing the code to guide development. * **Descriptive Test Names:** Use descriptive test names that clearly explain what is being tested. **Do This:** * Write unit tests that cover all possible scenarios, including edge cases and error conditions. * Refactor code to make it more testable (e.g., dependency injection). * Keep unit tests focused and avoid testing multiple units in a single test. * Mock external dependencies, like API calls or database interactions. **Don't Do This:** * Write tests that are too tightly coupled to the implementation details. * Skip testing error handling logic. * Put logic inside of React components. Move logic to utils to enable easy testing. ### 2.3. Code Examples #### Example 1: Unit Testing a Utility Function """javascript // app/utils/string-utils.ts export function toUpperCase(str: string): string { if (!str) { return ''; } return str.toUpperCase(); } // test/utils/string-utils.test.ts import { toUpperCase } from '~/utils/string-utils'; describe('toUpperCase', () => { it('should convert a string to uppercase', () => { expect(toUpperCase('hello')).toBe('HELLO'); }); it('should return an empty string if the input is null', () => { expect(toUpperCase(null as any)).toBe(''); }); it('should return an empty string if the input is undefined', () => { expect(toUpperCase(undefined as any)).toBe(''); }); }); """ #### Example 2: Unit Testing a React Component """jsx // app/components/Greeting.tsx import React from 'react'; interface GreetingProps { name: string; } export function Greeting({ name }: GreetingProps) { return <h1>Hello, {name}!</h1>; } // test/components/Greeting.test.tsx import React from 'react'; import { render, screen } from '@testing-library/react'; import { Greeting } from '~/components/Greeting'; describe('Greeting Component', () => { it('should render the greeting message with the provided name', () => { render(<Greeting name="World" />); const headingElement = screen.getByText(/Hello, World!/i); expect(headingElement).toBeInTheDocument(); }); }); """ #### Example 3: Mocking API calls using MSW in a Component Test """jsx // app/components/UserList.tsx import React, { useState, useEffect } from 'react'; interface User { id: number; name: string; } export function UserList() { const [users, setUsers] = useState<User[]>([]); useEffect(() => { async function fetchUsers() { const response = await fetch('/api/users'); const data = await response.json(); setUsers(data); } fetchUsers(); }, []); return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } // test/components/UserList.test.tsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { UserList } from '~/components/UserList'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get('/api/users', (req, res, ctx) => { return res(ctx.json([{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Doe' }])); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('UserList Component', () => { it('should render a list of users fetched from the API', async () => { render(<UserList />); await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Jane Doe')).toBeInTheDocument(); }); }); }); """ ## 3. Integration Testing Integration tests verify that multiple units work together correctly. These tests ensure that components and modules interact as expected. In Remix, this often means testing the interaction between components, data loaders, and actions. ### 3.1. Scope * Test the integration of React components with data loaders in "route" modules. * Focus on interactions between components in a specific area of the application. * Validate the flow of data between client-side components and server-side actions. ### 3.2. Best Practices * Use realistic test data that resembles actual production data. * Avoid mocking internal implementation details; focus on the public API. * Group related integration tests into logical suites. * Test both successful and error scenarios. **Do This:** * Test the interaction between a component and its data loader. * Test how form submissions trigger actions and update data. * Use a test database or in-memory data store to avoid polluting production data. **Don't Do This:** * Rely on mocks for everything; aim for some real integration. * Test individual units in isolation (that's for unit tests). * Make integration tests too broad; keep them focused. ### 3.3. Code Examples #### Example 1: Integration Testing a Component with a Data Loader """jsx // app/routes/users.tsx import { json, LoaderFunction } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; import React from 'react'; interface User { id: number; name: string; } export const loader: LoaderFunction = async () => { const users: User[] = [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Doe' }]; return json(users); }; export default function Users() { const users = useLoaderData<User[]>(); return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } // test/routes/users.test.tsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { RemixRoute } from '@remix-run/testing'; // Install: npm install --save-dev @remix-run/testing import UsersRoute, { loader } from '~/routes/users'; describe('Users Route', () => { it('should render a list of users fetched from the loader', async () => { const route = new RemixRoute({ path: '/users', module: { default: UsersRoute, loader, }, }); const { container } = render(await route.element()); // Await the element to resolve await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Jane Doe')).toBeInTheDocument(); }); }); }); """ #### Example 2: Integration Testing Form Submission and Action """jsx // app/routes/contact.tsx import { ActionFunction, json, LoaderFunction } from '@remix-run/node'; import { Form, useActionData } from '@remix-run/react'; import React from 'react'; interface ActionData { success?: boolean; errors?: { name?: string; email?: string; message?: string; }; } export const action: ActionFunction = async ({ request }) => { const formData = await request.formData(); const name = formData.get('name') as string; const email = formData.get('email') as string; const message = formData.get('message') as string; const errors: ActionData['errors'] = {}; if (!name) { errors.name = 'Name is required'; } if (!email) { errors.email = 'Email is required'; } if (!message) { errors.message = 'Message is required'; } if (Object.keys(errors).length > 0) { return json({ errors }, { status: 400 }); } // Simulate successful submission console.log('Form submitted:', { name, email, message }); return json({ success: true }); }; export default function Contact() { const actionData = useActionData<ActionData>(); return ( <Form method="post"> <div> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" /> {actionData?.errors?.name && <span>{actionData.errors.name}</span>} </div> <div> <label htmlFor="email">Email:</label> <input type="email" id="email" name="email" /> {actionData?.errors?.email && <span>{actionData.errors.email}</span>} </div> <div> <label htmlFor="message">Message:</label> <textarea id="message" name="message" /> {actionData?.errors?.message && <span>{actionData.errors.message}</span>} </div> <button type="submit">Submit</button> {actionData?.success && <span>Form submitted successfully!</span>} </Form> ); } // test/routes/contact.test.tsx import React from 'react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { RemixRoute } from '@remix-run/testing'; import ContactRoute, { action } from '~/routes/contact'; describe('Contact Route', () => { it('should display validation errors when the form is submitted with missing fields', async () => { const route = new RemixRoute({ path: '/contact', module: { default: ContactRoute, action, }, }); const { container } = render(await route.element()); fireEvent.click(screen.getByText('Submit')); await waitFor(() => { expect(screen.getByText('Name is required')).toBeInTheDocument(); expect(screen.getByText('Email is required')).toBeInTheDocument(); expect(screen.getByText('Message is required')).toBeInTheDocument(); }); }); it('should display a success message when the form is submitted successfully', async () => { const route = new RemixRoute({ path: '/contact', module: { default: ContactRoute, action, }, }); const { container } = render(await route.element()); fireEvent.change(screen.getByLabelText('Name:'), { target: { value: 'John Doe' } }); fireEvent.change(screen.getByLabelText('Email:'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByLabelText('Message:'), { target: { value: 'Hello!' } }); fireEvent.click(screen.getByText('Submit')); await waitFor(() => { expect(screen.getByText('Form submitted successfully!')).toBeInTheDocument(); }); }); }); """ ## 4. End-to-End (E2E) Testing E2E tests simulate real user interactions to validate the entire application flow, covering both the front-end and back-end. ### 4.1. Tools and Libraries * **Cypress:** (Recommended) A popular end-to-end testing framework with excellent developer experience and time travel debugging. * **Playwright:** Another powerful E2E framework for reliable cross-browser testing. Well documented and strongly supported by Microsoft. ### 4.2. Best Practices * **Focus on Critical Flows:** Test the most important user journeys, such as login, signup, and checkout. * **Use Realistic Data:** Use test data that resembles real production data. * **Avoid Over-Testing:** Don't over-test trivial UI elements; focus on functionality. * **Clean Up Data:** Ensure that test data is cleaned up after each test run to avoid interference. * **Environment Variables:** Configure your test environments using environment variables to manage different settings. **Do This:** * Simulate user interactions like clicking buttons, filling forms, and navigating pages. * Verify that data is correctly persisted and retrieved from the database. * Test error handling and edge cases. * Use a separate testing environment to avoid impacting production data. **Don't Do This:** * Test every single UI element; focus on critical flows. * Rely on hardcoded data; use test data factories. * Skip testing error scenarios. * Run E2E tests in production! ### 4.3. Code Examples (Cypress) #### Example 1: E2E Testing a Login Flow """javascript // cypress/e2e/login.cy.ts describe('Login Flow', () => { it('should allow a user to log in successfully', () => { cy.visit('/login'); cy.get('input[name="email"]').type('test@example.com'); cy.get('input[name="password"]').type('password'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); // Verify redirect cy.contains('Welcome to the dashboard!').should('be.visible'); }); it('should display an error message for invalid credentials', () => { cy.visit('/login'); cy.get('input[name="email"]').type('test@example.com'); cy.get('input[name="password"]').type('wrongpassword'); cy.get('button[type="submit"]').click(); cy.contains('Invalid credentials').should('be.visible'); }); }); """ #### Example 2: E2E Testing a Form Submission """javascript // cypress/e2e/contact.cy.ts describe('Contact Form', () => { it('should allow a user to submit the contact form successfully', () => { cy.visit('/contact'); cy.get('input[name="name"]').type('John Doe'); cy.get('input[name="email"]').type('john@example.com'); cy.get('textarea[name="message"]').type('Hello, this is a test message.'); cy.get('button[type="submit"]').click(); cy.contains('Form submitted successfully!').should('be.visible'); }); it('should display validation errors for missing fields', () => { cy.visit('/contact'); cy.get('button[type="submit"]').click(); cy.contains('Name is required').should('be.visible'); cy.contains('Email is required').should('be.visible'); cy.contains('Message is required').should('be.visible'); }); }); """ ## 5. Remix-Specific Testing Considerations Remix's unique data loading and form submission mechanisms require specific testing strategies: ### 5.1. Testing Loaders and Actions * **Mocking Data:** For unit tests, mock the data returned by loaders to isolate components. * **Form Data:** When testing actions, create "FormData" objects to simulate form submissions. Use "URLSearchParams" object in tests for get requests with params. * **Remix Test Utilities:** Use the "@remix-run/testing" package to render Remix routes in a test environment, making it easier to test loaders, actions, and components together. ### 5.2. Testing Route Components * **Context Providers:** Ensure that necessary context providers (e.g., "RemixBrowser") are available when testing route components. The "@remix-run/testing" package takes care of this. * **Navigation:** Test navigation events and routing logic using "useNavigate" hook. In E2E use "cy.visit" and "cy.go('back')". ### 5.3. Testing Server-Side Code * **Node Environment:** Ensure that your testing environment is configured to run Node.js code correctly. * **Database Connections:** Test interactions with your database using a test database or in-memory data store. Using a separate schema also works well for this purpose. ## 6. Continuous Integration (CI) Integrate testing into your CI pipeline to automatically run tests on every commit and pull request. ### 6.1. Tools * **GitHub Actions:** (Recommended) A CI/CD platform integrated directly into GitHub repositories. * **CircleCI:** A popular CI/CD platform with flexible configuration options. * **Jenkins:** An open-source CI/CD server that can be self-hosted. ### 6.2. Best Practices * **Run Tests in Parallel:** Use CI runners to run tests in parallel, reducing overall build time. * **Code Coverage Reports:** Generate code coverage reports and track coverage over time. * **Automated Deployments:** Configure CI to automatically deploy your application to staging or production environments after successful tests. * **Linting and Formatting:** Integrate linting and code formatting into your CI pipeline to enforce code style consistency. ## 7. Accessibility Testing Accessibility testing ensures that your Remix application is usable by people with disabilities. ### 7.1. Tools and Libraries * **axe-core:** An accessibility testing library that can be integrated into unit and E2E tests. * **eslint-plugin-jsx-a11y:** An ESLint plugin that checks for accessibility issues in JSX code. * **WAVE:** A web accessibility evaluation tool that provides visual feedback on accessibility issues. ### 7.2. Best Practices * **Automated Checks:** Use automated tools to identify common accessibility issues. * **Manual Testing:** Perform manual testing with assistive technologies like screen readers. * **Follow WCAG:** Adhere to the Web Content Accessibility Guidelines (WCAG) to ensure compliance. * **Semantic HTML:** Use semantic HTML elements to provide a clear and logical structure to your content. By following these testing methodologies standards, your Remix application can be more reliable, maintainable and provide a better user experience for everyone. Remember to adapt these guidelines to your specific project needs and technology stack.
# Performance Optimization Standards for Remix This document outlines coding standards and best practices specifically focused on performance optimization within Remix applications. Following these guidelines ensures optimal speed, responsiveness, and resource utilization, leading to a superior user experience. These standards are aligned with the latest Remix features and recommended patterns. ## 1. Data Loading and Caching Strategies Efficient data loading is crucial for perceived performance. Remix's focus on server-side rendering and data mutations provides unique opportunities for optimization. ### 1.1. Leverage Remix Loaders Effectively * **Do This:** Use loaders to fetch all necessary data server-side *before* rendering the route. This avoids client-side waterfalls and reduces time to first meaningful paint. * **Don't Do This:** Avoid fetching data client-side after the initial render unless absolutely necessary (e.g., progressive enhancement or user-initiated refreshes). **Why:** Server-side data fetching eliminates the need for additional round trips from the browser, dramatically improving initial load times. **Code Example:** """tsx // app/routes/products.$productId.tsx import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getProduct } from "~/models/product.server"; type LoaderData = { product: Awaited<ReturnType<typeof getProduct>>; }; export const loader: LoaderFunction = async ({ params }) => { const product = await getProduct(params.productId!); if (!product) { throw new Response("Not Found", { status: 404 }); } return json<LoaderData>({ product }); }; export default function ProductPage() { const { product } = useLoaderData<LoaderData>(); return ( <h1>{product.name}</h1> {product.description} ); } """ **Anti-Pattern:** Fetching product details inside the "ProductPage" component using "useEffect" or similar client-side techniques. ### 1.2. Implement Caching * **Do This:** Cache frequently accessed data at various levels: browser (HTTP caching), CDN, and server. * **Don't Do This:** Neglect caching strategies for commonly requested resources, leading to unnecessary server load and slower response times. **Why:** Caching reduces database queries and network latency for repeated requests. **Code Example (HTTP Caching):** In your loader, add cache headers: """tsx // app/routes/products.$productId.tsx import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getProduct } from "~/models/product.server"; type LoaderData = { product: Awaited<ReturnType<typeof getProduct>>; }; export const loader: LoaderFunction = async ({ params }) => { const product = await getProduct(params.productId!); if (!product) { throw new Response("Not Found", { status: 404 }); } return json<LoaderData>({ product }, { headers: { "Cache-Control": "public, max-age=3600", // Cache for 1 hour }, }); }; export default function ProductPage() { const { product } = useLoaderData<LoaderData>(); return ( <h1>{product.name}</h1> {product.description} ); } """ **Explanation:** The "Cache-Control" header instructs the browser and any intermediate caches (e.g., CDN) to store the response for a specified duration. Adjust "max-age" based on the data's volatility. ### 1.3. Stale-While-Revalidate (SWR) * **Do This:** Use SWR (Stale-While-Revalidate) patterns for data that can tolerate being slightly out-of-date. Return cached data immediately while revalidating in the background. * **Don't Do This:** Always fetch fresh data for every request when slightly stale data is acceptable. **Why:** SWR improves perceived performance by providing an instant response from the cache, even if the data is revalidated afterwards. **Code Example (Using "remix-utils" and custom caching):** """tsx // app/utils/cache.server.ts (example using Redis, but could be any cache) import { createClient } from 'redis'; const redisClient = createClient({ url: process.env.REDIS_URL, }); redisClient.connect().catch(console.error); export const getCached = async <T>(key: string): Promise<T | null> => { const value = await redisClient.get(key); return value ? JSON.parse(value) as T : null; }; export const setCached = async <T>(key: string, value: T, ttl: number = 3600): Promise<void> => { await redisClient.set(key, JSON.stringify(value), { EX: ttl }); }; // app/routes/products.$productId.tsx import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getProduct } from "~/models/product.server"; import { getCached, setCached } from "~/utils/cache.server"; type LoaderData = { product: Awaited<ReturnType<typeof getProduct>>; }; export const loader: LoaderFunction = async ({ params }) => { const productId = params.productId!; const cacheKey = "product:${productId}"; const cachedProduct = await getCached<Awaited<ReturnType<typeof getProduct>>>(cacheKey); if (cachedProduct) { // Revalidate in the background void getProduct(productId).then(freshProduct => { if (freshProduct) { setCached(cacheKey, freshProduct); } }); return json<LoaderData>({ product: cachedProduct }, { headers: { "Cache-Control": "public, max-age=60", // Short cache for the stale data }, }); } const product = await getProduct(productId); if (!product) { throw new Response("Not Found", { status: 404 }); } await setCached(cacheKey, product); // Cache the fresh data return json<LoaderData>({ product }, { headers: { "Cache-Control": "public, max-age=3600", // Cache for 1 hour for the fresh data }, }); }; export default function ProductPage() { const { product } = useLoaderData<LoaderData>(); return ( <h1>{product.name}</h1> {product.description} ); } """ **Explanation:** This example demonstrates manual SWR implementation. If cached data exists, it's returned immediately, and "getProduct" is called again to update the cache in the background. The initial response has a short "max-age" since we expect to update it soon. The refresh data gets a longer "max-age". Replace redis with your preferred caching datastore. ### 1.4. Data Normalization and Memoization * **Do This:** Normalize data structures returned from loaders to prevent redundant re-renders. Use memoization techniques (e.g., "useMemo", "React.memo") to avoid recomputing expensive values or re-rendering components unnecessarily. * **Don't Do This:** Pass raw, deeply nested data structures directly to components without normalization or memoization. **Why:** Data normalization and memoization minimize unnecessary React re-renders. **Code Example (Data Normalization):** """typescript // Example: Instead of returning a nested reviews array with user objects within each review, // return separate "reviews" and "users" objects, with "reviews" referencing user IDs. // Before (Nested): const data = { product: { id: 1, name: "Example Product", reviews: [ { id: 101, text: "Great product!", user: { id: 1, name: "Alice" } }, { id: 102, text: "Works well", user: { id: 2, name: "Bob" } }, ], }, }; // After (Normalized): const normalizedData = { products: { "1": { id: 1, name: "Example Product", reviewIds: [101, 102] }, }, reviews: { "101": { id: 101, text: "Great product!", userId: 1 }, "102": { id: 102, text: "Works well", userId: 2 }, }, users: { "1": { id: 1, name: "Alice" }, "2": { id: 2, name: "Bob" }, }, }; """ **Code Example (Memoization):** """tsx import React, { useMemo } from "react"; interface Props { data: { value: number; label: string }[]; onSelect: (value: number) => void; } const ExpensiveComponent: React.FC<Props> = React.memo(({ data, onSelect }) => { const calculatedValue = useMemo(() => { console.log("Calculating expensive value"); // Only logs when "data" changes return data.reduce((acc, item) => acc + item.value, 0); }, [data]); return ( {data.map((item) => ( onSelect(item.value)} key={item.value} > {item.label} ))} {calculatedValue} ); }); export default ExpensiveComponent; // Usage Example (Parent Component): function ParentComponent() { const [selected, setSelected] = React.useState<number | null>(null); const [data, setData] = React.useState([ { value: 1, label: "One" }, { value: 2, label: "Two" }, ]); const handleSelect = (value: number) => { setSelected(value); }; // Re-create the "data" array only when needed to avoid unnecessary re-renders of ExpensiveComponent. const updatedData = useMemo(() => data.map(item => ({...item, label: "${item.label} - Updated"})), [data]); return ( {selected ? "Selected: ${selected}" : "Nothing Selected"} setData([{ value: 3, label: "Three" },{ value: 4, label: "Four" }])} > Update Data ); } """ **Explanation:** "React.memo" prevents re-renders of "ExpensiveComponent" if the "data" prop hasn't changed (shallow comparison). "useMemo" only recalculates "calculatedValue" when the "data" array changes, and recreates the "updatedData" array only when the "data" array changes in the parent component, thus preventing unnecessary re-renders in both cases. ## 2. Code Splitting and Lazy Loading Remix makes code splitting simple. Take advantage of it! ### 2.1. Route-Based Code Splitting (Automatic) * **Do This:** Organize your application into routes. Remix automatically code-splits based on routes. * **Don't Do This:** Bundle all your application code into a single file. **Why:** Route-based code splitting ensures that users only download the code necessary for the current route. **Explanation:** Remix inherently supports route-based code splitting. Each route module is treated as a separate code chunk. This means that when a user navigates to a specific route, the browser only downloads the JavaScript, CSS, and other assets associated with that route. ### 2.2. Lazy Loading Components * **Do This:** Use "React.lazy" and "Suspense" to lazy-load non-critical components. Especially useful for components below the fold, or those that the user only sees after interacting with your page. * **Don't Do This:** Eagerly load all components, even those that are not immediately visible or necessary. **Why:** Lazy loading reduces the initial bundle size and improves initial load time. **Code Example:** """tsx import React, { lazy, Suspense } from "react"; const MapComponent = lazy(() => import("~/components/MapComponent")); function MyPage() { return ( <h1>My Page</h1> <Suspense fallback={Loading...}> <MapComponent /> </Suspense> ); } """ **Explanation:** "React.lazy" dynamically imports the "MapComponent". The "Suspense" component displays a fallback (e.g., a loading indicator) while the component is being loaded. The map component is only loaded when MyPage is rendered, and only starts downloading after MyPage is rendered, allowing the main content to load faster. ### 2.3. Dynamic Imports * **Do This:** Use dynamic imports ("import()") for modules that are not immediately required, like large utility libraries. * **Don't Do This:** Import all modules upfront, regardless of whether they are used on initial load. **Why:** Dynamic imports allow you to load modules on demand, reducing the initial bundle size. **Code Example:** """tsx async function handleClick() { const { format } = await import("date-fns"); const formattedDate = format(new Date(), "MM/dd/yyyy"); alert(formattedDate); } function MyComponent() { return ( Show Formatted Date ); } """ **Explanation:** The "date-fns" library is only loaded when the user clicks the button. This prevents including this library and its dependencies in the initial Javascript download. ## 3. Image Optimization Images are often the largest assets on a website. ### 3.1. Choose the Right Image Format * **Do This:** * Use WebP for general-purpose images. * Use AVIF where supported (check browser compatibility). * Use JPEG for photographs (if WebP/AVIF is not an option). * Use PNG for icons and images with transparency (when WebP/AVIF with alpha is not an option). * Prefer SVGs for vector graphics. * **Don't Do This:** Use BMP or TIFF formats on the web. Avoid using PNG or JPEG for vector graphics. **Why:** Modern formats like WebP and AVIF offer superior compression compared to older formats like JPEG and PNG, resulting in smaller file sizes and faster load times. SVGs are resolution-independent and can be scaled without loss of quality. ### 3.2. Compress Images * **Do This:** Use image optimization tools (e.g., ImageOptim, TinyPNG, squoosh.app) to compress images *before* uploading them to your project. Integrate image optimization into your build process. * **Don't Do This:** Use uncompressed or poorly compressed images. **Why:** Compression reduces file sizes without significant loss of quality. ### 3.3. Use Responsive Images * **Do This:** Use the "<picture>" element or the "srcset" attribute in "<img>" tags to serve different image sizes based on the user's screen size and device pixel ratio. * **Don't Do This:** Serve the same large image to all users, regardless of their device. **Why:** Responsive images ensure that users download only the necessary image resolution for their device, saving bandwidth and improving load times. **Code Example:** """tsx <picture> <source media="(max-width: 600px)" srcSet="image-small.webp" /> <source media="(max-width: 1200px)" srcSet="image-medium.webp" /> <img src="image-large.webp" alt="My Image" /> </picture> // OR <img src="image-medium.webp" srcSet="image-small.webp 480w, image-medium.webp 800w, image-large.webp 1200w" sizes="(max-width: 600px) 480px, (max-width: 800px) 800px, 1200px" alt="My Image" /> """ **Explanation:** The "<picture>" element allows you to specify different sources for different media queries. The "srcset" attribute on the "<img>" tag allows the browser to choose the most appropriate image based on screen width and pixel density. The "sizes" attribute provides hints to the browser about the image's intended display size, enabling more accurate selection. ### 3.4. Lazy Load Images * **Do This:** Use the "loading="lazy"" attribute on "<img>" tags to lazy load images that are not immediately visible in the viewport. * **Don't Do This:** Load all images eagerly, including those that are below the fold. **Why:** Lazy loading defers the loading of off-screen images until they are about to enter the viewport, improving initial page load time and reducing bandwidth consumption. **Code Example:** """tsx <img src="my-image.jpg" alt="My Image" loading="lazy" /> """ ## 4. Code Optimization and Rendering Writing efficient code minimizes execution time. ### 4.1. Avoid Unnecessary Re-renders * **Do This:** Use "React.memo", "useMemo", and "useCallback" to prevent unnecessary re-renders of components. * **Don't Do This:** Allow components to re-render unnecessarily, leading to wasted CPU cycles. **Why:** Minimizing re-renders improves the responsiveness of your application. (See memoization example above). ### 4.2. Minimize DOM Manipulations * **Do This:** Batch DOM updates where possible. Use techniques like "requestAnimationFrame" for animations. * **Don't Do This:** Perform frequent, individual DOM manipulations, which can lead to performance bottlenecks. **Why:** DOM manipulations are expensive. Batching and using "requestAnimationFrame" reduce the number of times the browser needs to reflow and repaint the screen. **Code Example (using "requestAnimationFrame"):** """tsx import React, { useRef } from 'react'; function AnimatedComponent() { const boxRef = useRef<HTMLDivElement>(null); const animate = () => { if (!boxRef.current) return; let start = null; const step = (timestamp: number) => { if (!start) start = timestamp; const progress = timestamp - start; if (boxRef.current) { boxRef.current.style.transform = "translateX(${Math.min(progress / 10, 200)}px)"; } if (progress < 2000) { requestAnimationFrame(step); } }; requestAnimationFrame(step); }; return ( Animate ); } export default AnimatedComponent; """ **Explanation:** This example uses "requestAnimationFrame" to perform the animation smoothly. "requestAnimationFrame" tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. This ensures the animation is synchronized with the browser's rendering pipeline, resulting in smoother animations. ### 4.3. Optimize JavaScript Execution - **Do This**: Use efficient algorithms and data structures. Profile your code to identify performance bottlenecks. Avoid computationally expensive operations in the main thread where possible. Explore Web Workers for offloading tasks. - **Don't Do This**: Write inefficient code that performs unnecessary calculations or operations. Neglect performance profiling. **Why**: Efficient algorithms and data structures reduce the time it takes to execute JavaScript code. Profiling helps you identify specific areas of your code that are slow. Web Workers allow you to run JavaScript code in a background thread, preventing it from blocking the main thread and impacting user interface responsiveness. ## 5. Remix-Specific Optimizations Leverage Remix's features to their fullest potential. ### 5.1. Prefetching * **Do This:** Use Remix's "useNavigation" hook to prefetch data for likely future routes as early as possible. * **Don't Do This:** Rely solely on the browser to fetch data on navigation, leading to slower perceived performance. **Why:** Prefetching anticipates user actions and fetches data in the background, making navigation feel instantaneous. **Code Example:** """tsx import { Form, useNavigation } from "@remix-run/react"; import { useEffect } from "react"; function MyLink({ to }: { to: string }) { const navigation = useNavigation(); useEffect(() => { if (navigation.state === "idle") { navigation.prefetch(to); } }, [to, navigation]); return ( <Form method="get" action={to}> {/* styling etc here */} </Form> ); } """ **Explanation:** The "useNavigation" hook provides information about the current navigation state. The "useEffect" hook triggers a prefetch of the target route ("to") when the navigation state is "idle", meaning the page is not currently loading or submitting data. Note that the example uses a "Form" component with method "get" for navigation. ### 5.2. Resource Route Optimization * **Do This:** Use resource routes for non-UI endpoints like API routes, webhooks, or image transformations. * **Don't Do This:** Handle all requests through standard route modules, even those that don't require a UI. **Why:** Resource routes allow you to separate UI routes from non-UI routes, improving organization and potentially optimizing server-side processing. ## 6. Monitoring and Performance Testing Continuous monitoring and testing are essential for maintaining optimal performance. ### 6.1. Implement Performance Monitoring * **Do This:** Tools like Lighthouse, WebPageTest, and browser developer tools should be used to audit your website’s performance. Use a real-user monitoring (RUM) solution to collect performance data from actual users. * **Don't Do This:** Don't rely solely on subjective impressions of performance. **Why:** Performance monitoring provides valuable insights into how your website is performing in real-world conditions. ### 6.2. Conduct Load Testing * **Do This:** Simulate high traffic loads to identify performance bottlenecks and ensure your application can handle peak demand. * **Don't Do This:** Assume your application can handle any load without testing. **Why:** Load testing helps you identify scalability issues and optimize your infrastructure. By adhering to these performance optimization standards, Remix developers can create applications that are fast, responsive, and provide a superior user experience. Regular monitoring and testing are essential to ensure that these standards are maintained throughout the application lifecycle.
# Code Style and Conventions Standards for Remix This document outlines the code style and conventions standards for Remix projects. Following these guidelines ensures consistency, readability, maintainability, and optimal performance across all aspects of our codebase. We aim for code that is not only functional but also easy to understand, debug, and extend. These patterns are based on the latest version of Remix and modern React development practices. ## 1. General Formatting and Style ### 1.1. Code Formatting * **Do This:** Use a code formatter like Prettier along with ESLint to automatically enforce consistent code style and formatting. * **Don't Do This:** Rely on manual formatting or inconsistent styles, which lead to code that is harder to read and maintain. **Why:** Consistent formatting significantly enhances code readability and reduces cognitive load, allowing developers to focus on the logic. Prettier automatically handles whitespace, line breaks, and indentation, while ESLint enforces coding rules and best practices. **Example Configuration (.prettierrc.js):** """javascript /** @type {import("prettier").Config} */ module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, }; """ **Example Configuration (.eslintrc.js):** """javascript module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', project: ['./tsconfig.json'], }, plugins: ['react', '@typescript-eslint', 'prettier'], rules: { 'prettier/prettier': 'error', 'react/react-in-jsx-scope': 'off', // Remix handles React import '@typescript-eslint/explicit-function-return-type': 'warn', // Encourage explicit types '@typescript-eslint/explicit-module-boundary-types': 'warn', // Encourage explicit types '@typescript-eslint/no-unused-vars': 'warn', }, settings: { react: { version: 'detect', }, }, }; """ ### 1.2. Naming Conventions * **Do This:** * Use "camelCase" for variables and function names. * Use "PascalCase" for component names. * Use "CONSTANT_CASE" for constants. * Use descriptive and meaningful names. * **Don't Do This:** * Use single-letter variable names unless within very short scopes. * Use ambiguous or misleading names. * Use abbreviations unless they are widely understood. **Why:** Clear naming makes code self-documenting and easier to understand at a glance. Consistent casing helps distinguish different types of identifiers. **Example:** """javascript // Good const userProfile = { ... }; function fetchUserData() { ... } const UserProfileCard = () => { ... }; const MAX_USERS = 100; // Bad const a = { ... }; function getData() { ... } const UPC = () => { ... }; const max = 100; """ ### 1.3. Comments and Documentation * **Do This:** Write JSDoc-style comments for functions, components, and complex logic. * **Do This:** Explain the purpose, parameters, and return value. * **Do This:** Keep comments up-to-date with changes in the code. * **Don't Do This:** Write excessive or redundant comments. The code itself should ideally be clear enough. * **Don't Do This:** Let comments become stale or misleading. **Why:** Well-written comments provide valuable context and help other developers understand the intent and functionality of the code. Modern IDEs leverage JSDoc to provide better code completion and documentation. **Example:** """javascript /** * Fetches user data from the server. * * @param {string} userId - The ID of the user to fetch. * @returns {Promise<UserProfile>} A promise that resolves to the user profile data. * @throws {Error} If the request fails. */ async function fetchUserData(userId: string): Promise<UserProfile> { try { const response = await fetch("/api/users/${userId}"); if (!response.ok) { throw new Error("Failed to fetch user data: ${response.status}"); } return await response.json(); } catch (error) { console.error("Error fetching user data:", error); // Log the error throw error; // Re-throw the error for handling upstream } } """ ### 1.4. Consistent Style * **Do This:** Maintain consistency within files and across the project. * **Do This:** Follow established patterns and conventions. * **Don't Do This:** Introduce unnecessary variation or stylistic inconsistencies. **Why:** A consistent style makes the codebase feel more unified and predictable, reducing cognitive overhead and making it easier to contribute. ## 2. Remix-Specific Conventions ### 2.1. File Structure * **Do This:** Organize your Remix project using a feature-based or route-based structure. * **Do This:** Group related files (components, styles, tests) within a feature directory. * **Do This:** Use the "app/" directory for all Remix-specific code (routes, components, utils, etc.) * **Don't Do This:** Scatter files haphazardly without a clear organizational scheme. * **Don't Do This:** Mix framework-specific code with application logic. **Why:** A well-defined file structure improves discoverability and maintainability, especially in larger projects. Features should be encapsulated and easy to locate. **Example (Feature-Based):** """ app/ ├── components/ │ ├── user-profile/ │ │ ├── UserProfileCard.tsx │ │ ├── UserProfileCard.css │ │ └── UserProfileCard.test.tsx │ └── ... ├── routes/ │ ├── users.$userId.tsx │ ├── index.tsx │ └── ... ├── utils/ │ └── api.ts └── root.tsx """ ### 2.2. Route Modules * **Do This:** Colocate route-specific components, data fetching logic, and styling within the route module. * **Do This:** Export a default React component for rendering the route. * **Do This:** Export "loader" and "action" functions for data loading and mutations, respectively. * **Don't Do This:** Define data loading logic outside of the "loader" function. * **Don't Do This:** Perform side effects or mutations directly within the component's render lifecycle. **Why:** Remix leverages route modules for code splitting and data loading. This keeps related logic together and optimizes performance. **Example:** """typescript jsx // app/routes/users.$userId.tsx import { useState, useEffect } from 'react'; import { json, LoaderFunctionArgs } from '@remix-run/node'; import { useLoaderData } from "@remix-run/react"; type UserProfile = { id: string; name: string; email: string; }; export async function loader({ params }: LoaderFunctionArgs) { const { userId } = params; try { const response = await fetch("https://api.example.com/users/${userId}"); if (!response.ok) { throw new Error("Failed to fetch user data: ${response.status}"); } const user: UserProfile = await response.json(); return json(user); } catch (error) { console.error("Error fetching user data:", error); throw new Error("Failed to fetch user data"); // Enhanced error for the client-side } } export default function UserProfileRoute() { const user = useLoaderData<typeof loader>(); if (!user) { return <div>Loading...</div>; // Or a more sophisticated loading indicator } return ( <div> <h1>User Profile</h1> <p>ID: {user.id}</p> <p>Name: {user.name}</p> <p>Email: {user.email}</p> </div> ); } """ ### 2.3. Data Loading and Mutations * **Do This:** Use "loader" functions for data fetching and "action" functions for data mutations. * **Do This:** Return data from "loader" and "action" functions as JSON using "Remix.json()". * **Do This:** Use "useLoaderData", "useActionData", and "useTransition" hooks to access data and manage form state. * **Do This:** Handle errors gracefully within "loader" and "action" functions and return an appropriate response (e.g., 404, 500). * **Don't Do This:** Use "useEffect" or other client-side data fetching techniques within route components. * **Don't Do This:** Mutate data directly within the render lifecycle. **Why:** Remix promotes server-side data loading for performance, SEO, and accessibility. Using "loader" and "action" functions ensures data is fetched on the server before rendering the component. **Example (Action):** """typescript jsx // app/routes/contact.tsx import { json, ActionFunctionArgs, FormData } from "@remix-run/node"; import { useActionData, useTransition } from "@remix-run/react"; type ActionData = { errors?: { name?: string; email?: string; message?: string; }; success?: boolean; }; export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const name = formData.get("name") as string; const email = formData.get("email") as string; const message = formData.get("message") as string; const errors: ActionData['errors'] = {}; if (!name) errors.name = "Name is required"; if (!email) errors.email = "Email is required"; if (!message) errors.message = "Message is required"; if (Object.keys(errors).length) { return json({ errors }); } // Simulate sending the email try { // In real-world scenario, implement email sending logic here with backend services console.log('Simulating sending email...'); await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate a network request delay console.log("Email sent successfully!\nName: ${name}\nEmail: ${email}\nMessage: ${message}"); return json({ success: true }); } catch (error) { console.error("Error sending email:", error); return json({ errors: { message: 'Failed to send email. Please try again.' } }, { status: 500 }); // Return a 500 status for server errors } }; export default function ContactRoute() { const actionData = useActionData<typeof action>() as ActionData; // Explicit TypeScript type const transition = useTransition(); return ( <form method="post"> {actionData?.errors?.message && ( <div className="error-message">{actionData.errors.message}</div> )} <div> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" /> {actionData?.errors?.name && ( <div className="error-message">{actionData.errors.name}</div> )} </div> <div> <label htmlFor="email">Email:</label> <input type="email" id="email" name="email" /> {actionData?.errors?.email && ( <div className="error-message">{actionData.errors.email}</div> )} </div> <div> <label htmlFor="message">Message:</label> <textarea id="message" name="message"></textarea> {actionData?.errors?.message && ( <div className="error-message">{actionData.errors.message}</div> )} </div> <button type="submit" disabled={transition.submission}> {transition.submission ? "Submitting..." : "Send"} </button> {actionData?.success && <div className="success-message">Message sent successfully!</div>} </form> ); } """ ### 2.4. Form Handling * **Do This:** Use Remix's built-in "<Form>" component for form submissions. * **Do This:** Handle form data in the "action" function using the "FormData" API. * **Do This:** Validate form data on the server-side and return errors to the client. * **Do This:** Use "useTransition" to display loading states during form submissions. * **Don't Do This:** Use traditional "<form>" elements with client-side event handlers. * **Don't Do This:** Perform client-side form validation only. **Why:** Remix's "<Form>" component integrates seamlessly with the framework's data loading and mutation mechanisms. Server-side validation ensures data integrity and security. ### 2.5. Error Handling * **Do This:** Catch errors in "loader" and "action" functions and return appropriate error responses. * **Do This:** Use "ErrorBoundary" components to handle unexpected errors during rendering. * **Do This:** Log errors to the server for monitoring and debugging. * **Don't Do This:** Let errors crash the application without proper handling. * **Don't Do This:** Expose sensitive error information to the client. **Why:** Robust error handling prevents application crashes and provides a better user experience. Error boundaries isolate failures and prevent them from propagating to other parts of the application. **Example (ErrorBoundary):** """typescript jsx // app/routes/users.$userId.tsx import { ErrorBoundaryComponent } from "@remix-run/react"; import { json, LoaderFunctionArgs } from '@remix-run/node'; export async function loader({ params }: LoaderFunctionArgs) { const { userId } = params; try { const response = await fetch("https://api.example.com/users/${userId}"); if (!response.ok) { throw new Response("User not found", { status: 404 }); } return await response.json(); } catch (error) { console.error("Error fetching user data:", error); throw new Error("Failed to fetch user data"); } } export default function UserProfileRoute() { // ... } export function ErrorBoundary() { return ( <div> <h1>Oops!</h1> <p>Something went wrong.</p> </div> ); } """ ### 2.6. Styling * **Do This:** Use CSS Modules, Tailwind CSS, or Styled Components for styling. * **Do This:** Colocate styles with the components they style. * **Do This:** Use a consistent naming convention for CSS classes. * **Don't Do This:** Use inline styles excessively. * **Don't Do This:** Write global CSS that can cause conflicts. **Why:** Modern styling techniques promote modularity, reusability, and maintainability. CSS Modules scope styles locally to prevent naming collisions. Tailwind CSS provides a utility-first approach with consistent design tokens. Styled Components allow you to write CSS-in-JS. **Example (CSS Modules):** """typescript jsx // app/components/UserProfile.module.css .container { border: 1px solid #ccc; padding: 16px; margin-bottom: 16px; } .name { font-size: 1.2em; font-weight: bold; } // app/components/UserProfile.tsx import styles from "./UserProfile.module.css"; export function UserProfile({ name, email }) { return ( <div className={styles.container}> <h2 className={styles.name}>{name}</h2> <p>Email: {email}</p> </div> ); } """ ## 3. React Best Practices ### 3.1. Functional Components and Hooks * **Do This:** Use functional components with React Hooks for managing state and side effects. * **Do This:** Keep components small and focused on a single responsibility. * **Do This:** Extract reusable logic into custom hooks. * **Don't Do This:** Use class components unless there's a specific reason. * **Don't Do This:** Write overly complex or monolithic components. **Why:** Functional components with Hooks are more concise, readable, and testable than class components. They promote code reuse and separation of concerns. **Example (Custom Hook):** """typescript jsx // util/useMounted.ts import { useState, useEffect } from 'react'; function useMounted(): boolean { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); return mounted; } export default useMounted; // app/components/MyComponent.tsx import useMounted from "../util/useMounted"; function MyComponent() { const mounted = useMounted(); if (!mounted) { return <div>Loading...</div>; } return <div>Component is now mounted!</div>; } """ ### 3.2. State Management * **Do This:** Use "useState" for local component state. * **Do This:** Use "useContext" for sharing state between components in a subtree. * **Do This:** Consider using a state management library like Zustand or Jotai for more complex application state. * **Don't Do This:** Overuse global state for component-local data. * **Don't Do This:** Mutate state directly. **Why:** Choosing the right state management solution is crucial for performance and maintainability. React's built-in hooks are sufficient for many use cases, while dedicated libraries offer more advanced features and optimizations. ### 3.3. Immutability * **Do This:** Treat state as immutable. * **Do This:** Use the spread operator (...) or "Object.assign()" to create new copies of objects and arrays when updating state. * **Don't Do This:** Modify state directly. **Why:** Immutability makes state updates predictable and prevents unexpected side effects. It also enables React to optimize rendering performance. **Example:** """javascript // Good const newUsers = [...users, newUser]; // Adding to array const updatedUser = { ...user, name: 'John' }; // Updating object // Bad users.push(newUser); // Mutating array user.name = 'John'; // Mutating object """ ### 3.4. Component Composition * **Do This:** Compose complex UIs from smaller, reusable components. * **Do This:** Use props to pass data and behavior to child components. * **Do This:** Use the "children" prop for rendering dynamic content within a component. * **Don't Do This:** Duplicate code across multiple components. * **Don't Do This:** Create deeply nested component hierarchies. **Why:** Component composition promotes code reuse and makes it easier to reason about the structure and behavior of the application. **Example:** """typescript jsx //components/Button.tsx type ButtonProps = { children: React.ReactNode; onClick: () => void; className?: string; }; function Button({ children, onClick, className }: ButtonProps) { return ( <button onClick={onClick} className={"default-button ${className || ''}"}> {children} </button> ); } // App.tsx function App() { return ( <Button onClick={() => alert('Clicked!')}> Click Me! </Button> ); } """ ## 4. Security Best Practices ### 4.1. Input Validation * **Do This:** Validate all user inputs on the server-side to prevent malicious data from entering the system. * **Do This:** Sanitize user inputs to prevent cross-site scripting (XSS) attacks. * **Don't Do This:** Rely solely on client-side validation. * **Don't Do This:** Trust user inputs without proper validation and sanitization. **Why:** Input validation is essential for protecting against security vulnerabilities. Server-side validation ensures that data meets the expected format and constraints, even if the client-side validation is bypassed. ### 4.2. Authentication and Authorization * **Do This:** Use a secure authentication library like Remix Auth or Auth0 for managing user authentication. * **Do This:** Implement proper authorization checks to ensure that users can only access the resources they are authorized to access. * **Do This:** Store passwords securely using a strong hashing algorithm like bcrypt. * **Don't Do This:** Store passwords in plain text. * **Don't Do This:** Implement authentication and authorization from scratch unless you have extensive security expertise. **Why:** Authentication and authorization are critical for protecting sensitive data and preventing unauthorized access. Using established libraries and following security best practices minimizes the risk of vulnerabilities. ### 4.3. Environment Variables * **Do This:** Store sensitive information like API keys and database credentials in environment variables. * **Do This:** Use a library like "dotenv" to manage environment variables during development. * **Don't Do This:** Hardcode sensitive information in the codebase. * **Don't Do This:** Commit ".env" files to the repository. **Why:** Environment variables provide a secure way to store sensitive information without exposing it in the codebase or repository. ## 5. Performance Optimization * **Do This**: Use Remix's built-in code splitting and prefetching features to optimize loading times. * **Do This**: Memoize expensive computations and components using "React.memo", "useMemo", and "useCallback". * **Do This**: Optimize images and other assets to reduce file sizes. * **Do This**: Profile the application using browser developer tools to identify performance bottlenecks. * **Don't Do This**: Load unnecessary data or code. * **Don't Do This**: Perform expensive calculations in the render lifecycle. ## 6. Testing * **Do This**: Write unit tests for components and utility functions using Jest and React Testing Library. * **Do This**: Write integration tests for testing the interaction between different parts of the application. * **Do This**: Write end-to-end tests using Cypress or Playwright to verify the application's overall functionality. * **Do This**: Aim for high test coverage to catch regressions and ensure code quality. * **Don't Do This**: Neglect testing or write superficial tests that don't provide meaningful coverage. ## 7. Accessibility * **Do This**: Use semantic HTML elements to provide a clear structure for screen readers and other assistive technologies. * **Do This**: Provide alternative text for images and other non-text content. * **Do This**: Ensure that the application is keyboard accessible. * **Do This**: Use ARIA attributes to enhance the accessibility of custom components. * **Don't Do This**: Create inaccessible interfaces that exclude users with disabilities. ## 8. Code Reviews * **Do This**: Conduct thorough code reviews to identify potential issues and ensure that the code meets the established standards. * **Do This**: Provide constructive feedback and suggestions for improvement. * **Do This**: Use a code review tool like GitHub pull requests or GitLab merge requests to facilitate the review process. * **Don't Do This**: Skip code reviews or perform them superficially. By adhering to these code style and convention standards, we can create maintainable, robust, and high-performing Remix applications.
# Security Best Practices Standards for Remix This document outlines the security best practices for building robust and secure Remix applications. These guidelines are designed to help developers avoid common vulnerabilities and implement secure coding patterns specific to the Remix framework. ## 1. General Security Principles ### 1.1. Principle of Least Privilege **Do This:** Grant users and processes only the minimum necessary permissions to perform their tasks. **Don't Do This:** Grant blanket permissions (e.g., admin access) when less privileged access would suffice. **Why:** Reduces the potential impact of security breaches. An attacker who gains access to a limited account has fewer opportunities to cause damage. **Example:** Implement role-based access control (RBAC) and restrict access to specific resources based on user roles. """typescript // app/routes/admin.tsx import { requireUser } from "~/utils/auth.server"; import { useLoaderData } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; export const loader = async ({ request }: LoaderArgs) => { const user = await requireUser(request, { requiredRole: "admin" }); return json({ message: "Admin Area", user }); }; export default function AdminRoute() { const { user } = useLoaderData<typeof loader>(); return ( <div> <h1>Admin Area</h1> <p>Welcome, {user.email}! You have admin privileges.</p> </div> ); } """ ### 1.2. Defense in Depth **Do This:** Implement multiple layers of security controls to protect against vulnerabilities. **Don't Do This:** Rely on a single security mechanism. **Why:** If one layer of security fails, other layers are still in place to prevent a breach. **Example:** Combine input validation, output encoding, and content security policy (CSP) to mitigate XSS attacks. ### 1.3. Secure by Default **Do This:** Configure applications with secure settings from the outset. **Don't Do This:** Rely on developers to remember to manually enable security features. **Why:** Reduces the risk of misconfiguration, which can lead to security vulnerabilities. **Example:** Set up "Strict-Transport-Security" (HSTS) headers at the server level and use secure cookies by default. ### 1.4. Regularly Update Dependencies **Do This:** Regularly update all dependencies of your Remix project, including Remix itself, React, and all third-party libraries. **Don't Do This:** Ignore dependency update notifications or postpone updates indefinitely. **Why:** Keeping dependencies up-to-date ensures that you're benefiting from the latest security patches and bug fixes. Outdated dependencies are a common target for attackers. **Tools:** Use tools like "npm audit" or "yarn audit" to identify vulnerable dependencies. Consider using automated dependency update services like Dependabot. ## 2. Cross-Site Scripting (XSS) Prevention ### 2.1. Understanding XSS XSS vulnerabilities occur when malicious scripts are injected into a website and executed by unsuspecting users. ### 2.2. Output Encoding **Do This:** Encode all user-supplied data before rendering it in the browser. Use appropriate encoding based on the output context (HTML, URL, JavaScript). **Don't Do This:** Directly render user input without proper escaping. **Why:** Prevents injected scripts from being executed by the browser. **Example:** Use React's built-in escaping mechanisms, or a dedicated library like "DOMPurify" for more complex scenarios. """typescript // app/components/Comment.tsx import { useEffect, useRef } from "react"; import DOMPurify from 'dompurify'; interface CommentProps { comment: string; } function Comment({ comment }: CommentProps) { const commentRef = useRef<HTMLDivElement>(null); useEffect(() => { if (commentRef.current) { commentRef.current.innerHTML = DOMPurify.sanitize(comment); } }, [comment]); return <div ref={commentRef}></div>; } export default Comment; """ ### 2.3. Content Security Policy (CSP) **Do This:** Implement a strict CSP header to control the sources from which the browser is allowed to load resources. **Don't Do This:** Use overly permissive CSP directives, such as "script-src: 'unsafe-inline' 'unsafe-eval'". **Why:** Limits the impact of XSS attacks by restricting the execution of unauthorized scripts. **Example:** Configure your server to send the "Content-Security-Policy" header. """ Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com; style-src 'self' https://example.com; img-src 'self' data:; """ ### 2.4. Preventing XSS in Remix Loaders and Actions Remix loaders and actions are server-side, but the data they provide ends up in the client. Ensure that any data originating from the user that is returned from a loader or action is appropriately sanitized/encoded if it's intended to be rendered as HTML. **Do This:** Sanitize user input before rendering, even if it's processed on the server. **Don't Do This:** Assume server-side processing automatically makes data safe for client-side rendering. **Example:** Sanitize a blog post's content before sending it to the client. """typescript // app/routes/blog/$slug.tsx import { json, LoaderArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getPost } from "~/models/post.server"; import DOMPurify from 'dompurify'; export const loader = async ({ params }: LoaderArgs) => { const post = await getPost(params.slug!); if (!post) { throw new Response("Not Found", { status: 404 }); } // Sanitize the post content before sending it to the client const sanitizedContent = DOMPurify.sanitize(post.content); return json({ title: post.title, content: sanitizedContent, }); }; export default function BlogPost() { const { title, content } = useLoaderData<typeof loader>(); return ( <div> <h1>{title}</h1> <div dangerouslySetInnerHTML={{ __html: content }}/> </div> ); } """ ## 3. Cross-Site Request Forgery (CSRF) Prevention ### 3.1. Understanding CSRF CSRF attacks occur when a malicious website tricks a user's browser into making unauthorized requests to a legitimate site on which the user is already authenticated. ### 3.2. CSRF Tokens **Do This:** Generate and validate CSRF tokens for all state-changing requests (e.g., POST, PUT, DELETE). **Don't Do This:** Rely solely on cookies for authentication, as they are automatically included in cross-site requests. Omit CSRF protection for seemingly "safe" operations. **Why:** Prevents attackers from forging requests on behalf of authenticated users. **Example:** Use a library like "csurf" or implement your own CSRF protection mechanism using Remix's "sessionStorage". """typescript // app/utils/csrf.server.ts import { createCookieSessionStorage, Session } from "@remix-run/node"; import { v4 as uuidv4 } from "uuid"; const sessionStorage = createCookieSessionStorage({ cookie: { name: "__session", httpOnly: true, path: "/", sameSite: "lax", secrets: ["YOUR_SESSION_SECRET"], secure: process.env.NODE_ENV === "production", }, }); async function createCSRFToken(session: Session): Promise<string> { const csrfToken = uuidv4(); session.set("csrfToken", csrfToken); return csrfToken; } async function validateCSRFToken(request: Request, session: Session): Promise<boolean> { const expectedToken = session.get("csrfToken"); const formData = await request.formData(); const actualToken = formData.get("_csrf"); if (!expectedToken || !actualToken || expectedToken !== actualToken) { return false; } return true; } export { sessionStorage, createCSRFToken, validateCSRFToken }; // app/routes/some-form.tsx import { Form, useActionData, useNavigation } from "@remix-run/react"; import { ActionArgs, json, LoaderArgs, redirect } from "@remix-run/node"; import { createCSRFToken, sessionStorage, validateCSRFToken } from "~/utils/csrf.server"; export const loader = async ({ request }: LoaderArgs) => { const session = await sessionStorage.getSession(request.headers.get("Cookie")); const csrfToken = await createCSRFToken(session); return json( { csrfToken }, { headers: { "Set-Cookie": await sessionStorage.commitSession(session), }, } ); }; type ActionData = { errors?: { message: string; }; }; export const action = async ({ request }: ActionArgs) => { const session = await sessionStorage.getSession(request.headers.get("Cookie")); if (!(await validateCSRFToken(request, session))) { return json<ActionData>( { errors: { message: "CSRF token is invalid" } }, { status: 400, headers: { "Set-Cookie": await sessionStorage.commitSession(session), }, } ); } // Process form data here const formData = await request.formData(); const data = { name: formData.get("name") as string, email: formData.get("email") as string, }; console.log("Form data", data); return redirect("/", { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, }); }; export default function SomeFormRoute() { const { csrfToken } = useLoaderData<typeof loader>(); const actionData = useActionData<typeof action>(); const navigation = useNavigation(); return ( <Form method="post"> <input type="hidden" name="_csrf" value={csrfToken} /> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" /> <label htmlFor="email">Email:</label> <input type="email" id="email" name="email" /> <button type="submit">Submit</button> {actionData?.errors?.message && ( <p style={{ color: "red" }}>{actionData.errors.message}</p> )} {navigation.state === "submitting" ? <p>Submitting...</p> : null} </Form> ); } """ ### 3.3. SameSite Cookies **Do This:** Configure cookies with the "SameSite" attribute set to "Lax" or "Strict" to prevent CSRF attacks. **Don't Do This:** Omit the "SameSite" attribute, which defaults to "None" in some browsers and provides no CSRF protection. Using "SameSite=None" without "Secure" attribute is also insecure. **Why:** Restricts when cookies are sent with cross-site requests. **Example:** Configure your session cookie with "SameSite: Lax". """typescript // remix.config.js /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { ignoredRouteFiles: ["**/.*"], serverDependenciesToBundle: ["marked"], serverModuleFormat: "cjs", future: { v2_routeConvention: true, v2_meta: true, v2_normalizeFormMethod:true, v2_errorBoundary: true }, }; // app/sessions.ts import { createCookieSessionStorage } from "@remix-run/node"; const sessionSecret = process.env.SESSION_SECRET; if (!sessionSecret) { throw new Error("SESSION_SECRET must be set"); } export const sessionStorage = createCookieSessionStorage({ cookie: { name: "my_session", secure: process.env.NODE_ENV === "production", secrets: [sessionSecret], sameSite: "lax", path: "/", httpOnly: true, }, }); export const { getSession, commitSession, destroySession } = sessionStorage; """ ## 4. Authentication and Authorization ### 4.1. Secure Password Storage **Do This:** Hash passwords using a strong hashing algorithm (e.g., bcrypt, scrypt, Argon2) with a unique salt for each password. **Don't Do This:** Store passwords in plain text or use weak hashing algorithms like MD5 or SHA1. **Why:** Protects passwords from being compromised in the event of a data breach. **Example:** Utilize a library like "bcrypt" to hash and compare passwords. """typescript // app/utils/auth.server.ts import bcrypt from "bcryptjs"; const saltRounds = 10; export async function hashPassword(password: string): Promise<string> { return bcrypt.hash(password, saltRounds); } export async function verifyPassword(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash); } """ ### 4.2. Multi-Factor Authentication (MFA) **Do This:** Implement MFA to add an extra layer of security to the login process. **Don't Do This:** Rely solely on passwords for authentication. **Why:** Makes it more difficult for attackers to gain access to user accounts, even if they obtain the password. **Example:** Integrate with a third-party MFA provider (e.g., Authy, Google Authenticator) or implement your own MFA mechanism using time-based one-time passwords (TOTP). ### 4.3. Session Management **Do This:** Use secure cookies with appropriate attributes (e.g., "HttpOnly", "Secure", "SameSite") to store session identifiers. Implement session timeout and renewal mechanisms. **Don't Do This:** Store sensitive information in cookies or rely on predictable session identifiers. **Why:** Protects session data from being accessed by unauthorized parties and limits the lifespan of stolen session identifiers. **Example:** Configure your session storage using Remix's "createCookieSessionStorage". """typescript // app/sessions.ts import { createCookieSessionStorage } from "@remix-run/node"; const sessionSecret = process.env.SESSION_SECRET; if (!sessionSecret) { throw new Error("SESSION_SECRET must be set"); } export const sessionStorage = createCookieSessionStorage({ cookie: { name: "__session", secure: process.env.NODE_ENV === "production", secrets: [sessionSecret], sameSite: "lax", path: "/", httpOnly: true, maxAge: 60 * 60 * 24 * 7, // 7 days }, }); export const { getSession, commitSession, destroySession } = sessionStorage; """ ### 4.4. Authorization Checks **Do This:** Implement robust authorization checks to ensure that users can only access resources and perform actions that they are authorized to. **Don't Do This:** Rely on client-side checks for authorization, as they can be easily bypassed. Assume authentication implies authorization. **Why:** Prevents unauthorized access to sensitive data and functionality. **Example:** Implement role-based access control (RBAC) and perform authorization checks in your route loaders and actions. """typescript // app/utils/auth.server.ts import { sessionStorage } from "~/sessions"; import { redirect } from "@remix-run/node"; export async function requireUser( request: Request, { redirectTo, requiredRole }: { redirectTo?: string; requiredRole?: string } = {} ) { const session = await sessionStorage.getSession( request.headers.get("Cookie") ); const userId = session.get("userId"); const userRole = session.get("role"); // Example: "admin", "user", etc. if (!userId) { throw redirect(redirectTo || "/login"); } if (requiredRole && userRole !== requiredRole) { throw new Response("Unauthorized", { status: 403 }); // Or redirect to unauthorized page } return { userId, email: session.get("email") , role: userRole}; } // app/routes/admin.tsx import { useLoaderData } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { requireUser } from "~/utils/auth.server"; export const loader = async ({ request }: LoaderArgs) => { const user = await requireUser(request, { requiredRole: 'admin' }); return json({ message: "Admin Area", user }); }; export default function AdminRoute() { const { user } = useLoaderData<typeof loader>(); return ( <div> <h1>Admin Area</h1> <p>Welcome, {user.email}! You have admin privileges.</p> </div> ); } """ ## 5. Input Validation and Sanitization ### 5.1. Validate All User Input **Do This:** Validate all user-supplied data on both the client-side and server-side. Implement strong validation rules to ensure that data conforms to expected formats and ranges. **Don't Do This:** Rely solely on client-side validation, as it can be easily bypassed by attackers. Trust data from external sources. **Why:** Prevents invalid data from being processed by the application, which can lead to errors or security vulnerabilities. **Example:** Use libraries like Zod or Yup for schema validation. """typescript // app/utils/validation.ts import { z } from "zod"; export const userSchema = z.object({ email: z.string().email(), password: z.string().min(8), age: z.number().min(18).max(120).optional(), }); export type User = z.infer<typeof userSchema>; // app/routes/register.tsx import { Form, useActionData } from "@remix-run/react"; import { ActionArgs, json, redirect } from "@remix-run/node"; import { createUser } from "~/models/user.server"; import { userSchema } from "~/utils/validation"; import { hashPassword } from "~/utils/auth.server"; import { sessionStorage } from "~/sessions"; export const action = async ({ request }: ActionArgs) => { const formData = await request.formData(); const email = formData.get("email"); const password = formData.get("password"); const result = userSchema.safeParse({ email, password }); if (!result.success) { const issues = result.error.issues.map(issue => ({ path: issue.path.join('.'), message: issue.message, })); return json({ errors: issues }, { status: 400 }); } const hashedPassword = await hashPassword(result.data.password); await createUser(result.data.email, hashedPassword); const session = await sessionStorage.getSession(request.headers.get("Cookie")); session.set('email', result.data.email); return redirect('/',{ headers:{ "Set-Cookie": await sessionStorage.commitSession(session) } } ) }; export default function RegisterRoute() { const actionData = useActionData<typeof action>(); return ( <Form method="post"> <label htmlFor="email">Email:</label> <input type="email" id="email" name="email" /> <label htmlFor="password">Password:</label> <input type="password" id="password" name="password" /> <button type="submit">Register</button> {actionData?.errors?.map(error => ( <p key={error.path} style={{ color: "red" }}> {error.path}: {error.message} </p> ))} </Form> ); } """ ### 5.2. Sanitize User Input **Do This:** Sanitize user input to remove or escape potentially malicious characters before storing it in the database or displaying it to other users. **Don't Do This:** Store unsanitized user input, as it can lead to XSS or other vulnerabilities. **Why:** Prevents attackers from injecting malicious code into the application. **Example:** Use a library like "DOMPurify" to sanitize HTML input. (See XSS section for an example.) ### 5.3. Validate File Uploads **Do This:** Validate file uploads to ensure that only allowed file types are uploaded and that the files do not contain malicious content. **Don't Do This:** Allow users to upload arbitrary files without validation, as this can lead to remote code execution vulnerabilities. **Why:** Prevents attackers from uploading malicious files that can compromise the server. **Example:** Check the file extension, MIME type, and file size. Also, consider scanning uploaded files for malware. ## 6. Data Handling and Storage ### 6.1. Secure Database Connections **Do This:** Use parameterized queries or prepared statements to prevent SQL injection attacks. **Don't Do This:** Concatenate user input directly into SQL queries. **Why:** Protects the database from being compromised by malicious SQL code. **Example:** Use your database driver's built-in support for parameterized queries. If using Prisma: """typescript // app/models/user.server.ts import { prisma } from "~/db.server"; export async function findUserByEmail(email: string) { return prisma.user.findUnique({ where: { email: email, }, }); } """ ### 6.2. Encrypt Sensitive Data **Do This:** Encrypt sensitive data at rest and in transit. **Don't Do This:** Store sensitive data in plain text. **Why:** Protects sensitive data from being accessed by unauthorized parties. **Example:** Use TLS/SSL to encrypt data in transit and use a strong encryption algorithm (e.g., AES) to encrypt data at rest. ### 6.3. Secure Logging **Do This:** Implement secure logging practices to protect sensitive information from being exposed in log files. **Don't Do This:** Log sensitive information, such as passwords or credit card numbers. **Why:** Prevents sensitive data from being compromised if log files are accessed by unauthorized parties. **Example:** Redact or mask sensitive data before logging it. Consider using structured logging to make log analysis easier. ### 6.4 Rate Limiting **Do This: ** Implement rate limiting to protect against brute-force attacks, denial-of-service (DoS) attacks, and other abusive behaviors. Apply rate limits to API endpoints, login attempts, and other critical operations. **Don't Do This:** Fail to implement rate limits, leaving your application vulnerable to automated attacks. Use overly generous rate limits that do not provide adequate protection. **Why:** Rate limiting helps to prevent attackers from overwhelming your server and gaining unauthorized access to resources. **Example:** Use a middleware or library to implement rate limiting based on IP address, user ID, or other criteria. """typescript // app/utils/rate-limit.ts import { RateLimiterMemory } from 'rate-limiter-flexible'; const rateLimiter = new RateLimiterMemory({ points: 5, // 5 points duration: 60, // Per 60 seconds }); export async function rateLimit(req: Request) { try { await rateLimiter.consume(req.ip); // Consume 1 point per request from IP } catch (rejRes) { //console.log('Too Many Requests'); return false; } return true; } // Example Remix action with rate limiting import { ActionArgs, json } from "@remix-run/node"; import { rateLimit } from "~/utils/rate-limit"; export async function action({ request }: ActionArgs) { if (!await rateLimit(request)) { return json({ error: "Too many requests, please try again later." }, { status: 429 }); } // ... process the action } """ ## 7. Remix-Specific Security Considerations ### 7.1. Server-Side Rendering (SSR) and Data Exposure Be particularly careful with data fetched in loaders when it involves user-specific information or secrets. While Remix handles SSR efficiently, ensure you don't inadvertently expose sensitive data in the initial HTML payload if it's not meant to be visible to all users. **Do This:** Carefully consider what data needs to be rendered on the server and what can be fetched on the client after initial load. Use "useHydrated" hook to delay rendering elements that depend on client-side data. **Don't Do This:** Include sensitive user information directly in the initial HTML if it's only relevant after authentication and not meant for search engines. ### 7.2. Environment Variables **Do This:** Store sensitive information, like API keys and database credentials, in environment variables and access them using "process.env". **Don't Do This:** Hardcode sensitive information directly in your code. **Why:** Prevents sensitive information from being exposed in your codebase and allows you to easily change credentials without modifying your code. **Example:** """typescript // .env DATABASE_URL="your_database_url" API_KEY="your_api_key" // app/utils/db.server.ts const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) { throw new Error("DATABASE_URL must be set"); } """ ### 7.3. Remix Config **Do This:** Review your remix.config.js carefully. Ensure you understand the implications of any custom server configurations or build settings. **Don't Do This:** Use overly permissive "serverDependenciesToBundle". This can increase your bundle size and potentially introduce security vulnerabilities if you're bundling packages that should not be exposed. ### 7.4. Route File Placement and Security Remix's file-based routing makes it easy to define routes. Be careful to not expose internal routes or files. **Do This:** Place route files within the "app/routes" directory and secure them with appropriate authentication and authorization checks. **Don't Do This:** Accidentally expose sensitive internal routes or data files by placing them in the "/public" directory. ### 7.5. Leverage Remix's Security Features Remix is constantly evolving, keep abreast of new security features and recommendations in the official documentation and release notes. **Do This:** Regularly check Remix release notes for new security features or recommendations and incorporate them into your development process. **Don't Do This:** Assume that older versions of Remix are inherently secure. Stay up-to-date with the latest releases and security patches. ## 8. Testing and Auditing ### 8.1. Security Testing **Do This:** Perform regular security testing to identify and address vulnerabilities in your application. **Don't Do This:** Rely solely on manual code reviews for security testing. **Why:** Helps to identify vulnerabilities that may not be apparent during development. **Example:** Use automated security scanning tools and penetration testing to identify vulnerabilities. Use tools like "npm audit" and "yarn audit" to check for vulnerabilities in your dependencies. ### 8.2. Code Reviews **Do This:** Conduct thorough code reviews to identify potential security vulnerabilities and ensure that code adheres to security best practices. **Don't Do This:** Skip code reviews or assign them to inexperienced developers. **Why:** Helps to catch security vulnerabilities early in the development process. ### 8.3. Security Audits **Do This:** Conduct regular security audits of your application to identify and address security vulnerabilities. **Don't Do This:** Assume your application is secure after initial development and deployment. **Why:** Helps ensure ongoing security by proactively identifying and addressing new vulnerabilities as they arise. ## 9. Conclusion Following these security best practices will help you build robust and secure Remix applications that are resistant to common security vulnerabilities. Remember that security is an ongoing process, and it's important to stay up-to-date with the latest security threats and best practices. Prioritize training and awareness for developers to ensure a security-conscious development culture.
# State Management Standards for Remix This document outlines the coding standards for state management in Remix applications. These standards aim to promote maintainability, performance, and a predictable data flow within your Remix projects. We will focus on leveraging Remix's core concepts and APIs for optimal state management, while also considering client-side state solutions when appropriate. ## 1. General Principles ### 1.1. Embrace Remix's Data Flow **Do This:** * Favor server-side data loading and mutations via Remix loaders and actions. * Utilize Remix's "useLoaderData", "useActionData", and "useTransition" hooks to access and react to data changes. * Understand that the server is the single source of truth for your application's data wherever possible. **Don't Do This:** * Reach directly into client-side stores to fetch data that should be loaded from the server. * Bypass Remix's loaders and actions for data fetching and mutations. * Over-rely on client-side state for global application data. **Why:** Remix is designed around the concept of progressively enhanced server-side rendering. Deviating from this model often leads to difficult-to-debug state inconsistencies, degraded performance, and a less accessible experience for users. ### 1.2. Minimize Client-Side State **Do This:** * Limit client-side state primarily to UI-specific concerns (e.g., form field values, modal visibility, local user preferences). * Strive to derive or calculate as much client-side state as possible from the server-provided data. * Consider using "useRef" for transient state that doesn't require re-renders. **Don't Do This:** * Store large amounts of infrequently changing data in client-side state management solutions. * Duplicate data already managed on the server in the client. * Use client-side state as a primary mechanism for coordinating data between different parts of the application when server-side solutions exist. **Why:** Excessive reliance on client-side state introduces complexity, increases bundle sizes, and can negatively affect performance. Remix applications should strive to be as lean as possible on the client. ### 1.3. Data Co-location **Do This:** * Load data as close as possible to where it's consumed using "useLoaderData". * Place data mutations (actions) in the same route module where the corresponding UI component resides. * Leverage nested routes to further subdivide data loading and mutation logic. **Don't Do This:** * Load data at the top level of your application and pass it down through props. * Centralize all data mutations in a single massive action function. * Ignore Remix's routing capabilities and try to replicate a global state management system. **Why:** Co-location improves code organization, reduces the likelihood of prop drilling, and makes your application's data dependencies more explicit. This makes code easier to understand, refactor, and test. ### 1.4. Optimistic Updates **Do This:** * Implement optimistic updates to improve the perceived responsiveness of your application. * Use "useTransition" to track the state of pending mutations and update the UI accordingly. * Handle potential errors gracefully by reverting optimistic updates if the mutation fails. **Don't Do This:** * Assume that all mutations will succeed. * Visually "freeze" the UI while waiting for a mutation to complete. * Introduce complex client-side logic to manage optimistic updates. Leverage Remix "useFetcher" and "Form" for easier implementations. **Why:** Optimistic updates provide a better user experience by making your application feel faster and more responsive. ## 2. Server-Side Data Loading and Mutations ### 2.1. Loaders **Do This:** * Use loaders to fetch all data required by a route segment before rendering. * Return plain JavaScript objects or arrays from your loaders (serializeable data). * Use the "json" helper function from "@remix-run/node" to ensure proper Content-Type headers. * Handle errors in your loaders and return informative error responses. **Don't Do This:** * Perform side effects in loaders. Loaders should only fetch data. * Return complex data structures from your loaders that are difficult to serialize. * Forget to set the Content-Type header when returning data. **Example:** """tsx // app/routes/posts/$postId.tsx import { json, LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getPost } from "~/models/post.server"; // Assuming you have a post model export const loader = async ({ params }: LoaderFunctionArgs) => { const { postId } = params; try { const post = await getPost(postId); if (!post) { throw new Response("Post not found", { status: 404 }); } return json({ post }); } catch (error: any) { console.error("Error fetching post:", error); throw new Response("Failed to fetch post", { status: 500 }); } }; export default function PostRoute() { const { post } = useLoaderData<typeof loader>(); return ( <div> <h1>{post.title}</h1> <p>{post.body}</p> </div> ); } """ ### 2.2. Actions **Do This:** * Use actions to handle all data mutations (e.g., creating, updating, deleting). * Return a "redirect" from your action whenever the mutation requires a navigation. * Return data from your action to provide feedback to the user or update client-side state. * Use the "Form" component from "@remix-run/react" to submit data to your actions. * Handle errors in your actions and return informative error responses. **Don't Do This:** * Perform data fetching in actions. Actions should only mutate data. * Mutate data directly in your components. * Forget to handle errors in your actions. **Example:** """tsx // app/routes/posts/new.tsx import { ActionFunctionArgs, json, redirect } from "@remix-run/node"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { createPost } from "~/models/post.server"; export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const title = formData.get("title") as string; const body = formData.get("body") as string; if (!title || !body) { return json( { errors: { title: !title ? "Title is required" : null, body: !body ? "Body is required" : null } }, { status: 400 } ); } try { const post = await createPost({ title, body }); return redirect("/posts/${post.id}"); } catch (error: any) { console.error("Error creating post:", error); return json( { errors: { form: "Failed to create post" } }, { status: 500 } ); } }; export default function NewPostRoute() { const actionData = useActionData<typeof action>(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; return ( <div> <h1>Create New Post</h1> <Form method="post"> <div> <label htmlFor="title">Title:</label> <input type="text" id="title" name="title" defaultValue={actionData?.title} /> {actionData?.errors?.title && <span>{actionData.errors.title}</span>} </div> <div> <label htmlFor="body">Body:</label> <textarea id="body" name="body" defaultValue={actionData?.body} /> {actionData?.errors?.body && <span>{actionData.errors.body}</span>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Creating..." : "Create Post"} </button> {actionData?.errors?.form && <span>{actionData.errors.form}</span>} </Form> </div> ); } """ ### 2.3. "useFetcher" for Background Updates **Do This:** * Use "useFetcher" to perform background data updates without navigating away from the current page. This is ideal for things like polling for updates. * Use "fetcher.load" to trigger a loader. * Use "fetcher.Form" to submit data to an action. * Utilize "fetcher.data" to access the result of the loader or action. * Check "fetcher.state" to understand the current state of the fetcher (idle, loading, submitting). **Don't Do This:** * Use "useFetcher" as a replacement for standard loaders and actions. * Overuse "useFetcher" for every data interaction, as it adds complexity. **Example:** """tsx // app/routes/admin.tsx import { useFetcher } from "@remix-run/react"; import { useEffect } from "react"; export default function AdminRoute() { const fetcher = useFetcher(); useEffect(() => { if (fetcher.state === "idle") { fetcher.load("/admin/data"); // Assuming you have a loader at this route } }, [fetcher.state, fetcher.load]); return ( <div> <h1>Admin Dashboard</h1> {fetcher.state === "loading" ? ( <p>Loading data...</p> ) : ( <pre>{JSON.stringify(fetcher.data, null, 2)}</pre> )} <fetcher.Form method="post" action="/admin/update"> <input type="hidden" name="action" value="performUpdate" /> <button type="submit" disabled={fetcher.state === 'submitting'}> {fetcher.state === 'submitting' ? 'Updating...' : 'Update Data'} </button> </fetcher.Form> </div> ); } """ ## 3. Client-Side State Management ### 3.1. Built-in Hooks ("useState", "useReducer", "useRef") **Do This:** * Use "useState" for simple, local component state. * Use "useReducer" for more complex state logic or when the next state depends on the previous state. * Use "useRef" for storing mutable values that don't trigger re-renders (e.g., DOM nodes, timers). Great for maintaining scroll positions across reloads. **Don't Do This:** * Overuse "setState" for complex updates. * Mutate state directly without using the setter function. * Store large amounts of data in "useRef" that should be managed by a state management library or server-side. **Example:** """tsx import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } """ ### 3.2. Context API **Do This:** * Use the Context API to share state between components that are far apart in the component tree. * Create custom hooks to access context values and provide a more convenient API. * Keep context providers as close as possible to the components that consume the context. Use multiple specialized contexts over one large context. **Don't Do This:** * Use the Context API for global application state that should be managed by Remix's data loading mechanisms. * Over-rely on the Context API, as it can make your components more difficult to test and reuse. **Example:** """tsx // app/context/ThemeContext.tsx import { createContext, useState, useContext } from "react"; type Theme = "light" | "dark"; interface ThemeContextProps { theme: Theme; toggleTheme: () => void; } const ThemeContext = createContext<ThemeContextProps | undefined>(undefined); export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState<Theme>("light"); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; } // app/components/ThemeSwitcher.tsx import { useTheme } from "~/context/ThemeContext"; export default function ThemeSwitcher() { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme}> Switch to {theme === "light" ? "Dark" : "Light"} Theme </button> ); } """ ### 3.3. Third-Party State Management Libraries (When Necessary) While Remix encourages using server-side data loading for primary state management, there may be scenarios where a client-side library is beneficial, specifically for complex UI state or offline persistence. **When to Consider:** * Highly interactive UIs with complex client-side logic. * Offline data caching and persistence requirements. **Recommended Libraries:** * **TanStack Query:** Excellent for managing server state and data fetching with advanced caching and invalidation features. Complements Remix's data loading beautifully. * **Jotai or Zustand:** Simple and lightweight state management libraries for managing UI state (use sparingly). **Do This (If Using a Library):** * Select a library that aligns with Remix's architecture and minimizes client-side overhead. TanStack Query is often the first choice. * Encapsulate library-specific logic within custom hooks to isolate dependencies. * Avoid mixing server-side data fetching with client-side library-based data fetching in the same component wherever possible. **Don't Do This:** * Introduce a client-side state management library as the *default* approach. Start with Remix's built-in data loading. * Use a heavy-weight state management library (like Redux) without carefully considering the alternatives. * Create tight coupling between your UI components and the chosen library. **Example (TanStack Query):** """tsx // app/utils/queries.ts import { useQuery } from "@tanstack/react-query"; async function fetchTodos() { const response = await fetch("/api/todos"); // Assuming an API endpoint if (!response.ok) { throw new Error("Failed to fetch todos"); } return response.json(); } export function useTodos() { return useQuery({ queryKey: ["todos"], queryFn: fetchTodos, }); } // app/routes/todos.tsx import { useTodos } from "~/utils/queries"; export default function TodosRoute() { const { data: todos, isLoading, isError, error } = useTodos(); if (isLoading) { return <p>Loading todos...</p>; } if (isError) { return <p>Error: {error?.message}</p>; } return ( <ul> {todos?.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); } """ ## 4. Form State Management Remix's "Form" component and "useNavigation" hook provide excellent tools for form state management with minimal client-side code. **4.1 Basic Form Handling** """tsx import { Form, useNavigation } from "@remix-run/react"; export default function MyForm() { const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; return ( <Form method="post" action="/my-action"> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Submitting..." : "Submit"} </button> </Form> ); } """ **4.2 Controlled Form Components (If Necessary)** For advanced form scenarios, such as masked inputs or complex validation, consider using controlled components with "useState". However, strive to minimize the need for controlled components. If the server can handle validation and provide feedback, that is often preferable. """tsx import { useState } from "react"; import { Form, useNavigation } from "@remix-run/react"; export default function ControlledForm() { const [inputValue, setInputValue] = useState(""); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; return ( <Form method="post" action="/controlled-action"> <label htmlFor="input">Input:</label> <input type="text" id="input" name="input" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Submitting..." : "Submit"} </button> </Form> ); } """ ## 5. Error Handling **Do This:** * Implement comprehensive error handling in both loaders and actions. * Use "ErrorBoundary" components to catch errors at the route level. * Return informative error responses to be displayed to the user. * Log errors on the server for debugging purposes. **Don't Do This:** * Ignore errors. * Rely solely on client-side error handling. * Expose sensitive information in error messages. **Example (ErrorBoundary):** """tsx // app/routes/posts/$postId.tsx import { json, LoaderFunctionArgs } from "@remix-run/node"; import { useRouteError } from "@remix-run/react"; import { getPost } from "~/models/post.server"; export const loader = async ({ params }: LoaderFunctionArgs) => { const { postId } = params; const post = await getPost(postId); if (!post) { throw new Response("Post not found", { status: 404 }); } return json({ post }); }; export function ErrorBoundary() { const error = useRouteError(); if (error instanceof Error) { return ( <div> <h1>Error!</h1> <p>{error.message}</p> </div> ); } return ( <div> <h1>Oops!</h1> <p>Something went wrong.</p> <p>Details:{JSON.stringify(error)}</p> </div> ); } """ ## 6. Data Invalidation Data invalidation ensures that your application displays the most up-to-date information after a mutation occurs. Remix provides several mechanisms for data invalidation. **Do This:** * Use "redirect" from your action to trigger a re-fetch of the data in the target route. * If not redirecting, return data from your action that can be used to update the UI on the client. * Leverage "useFetcher" when you need to update data in the background without a full page reload. **Don't Do This:** * Manually manipulate the client cache. Let Remix handle data invalidation. * Rely on the user to manually refresh the page to see updated data. This coding standard document should serve as a comprehensive guide for managing state effectively in Remix applications. By adhering to these standards, you can build robust, maintainable, and performant applications that leverage the full power of the Remix framework. Remember to keep this document up-to-date as Remix evolves and new best practices emerge.