# 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'
# 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.
# Core Architecture Standards for Remix This document outlines the core architecture standards for Remix projects. Adhering to these standards will ensure maintainability, scalability, performance, and security across Remix applications. This focuses on the fundamental architectural patterns, project structure, and organization principles directly related to Remix's unique capabilities. ## 1. Project Structure and Organization Establishing a clear and consistent project structure is crucial for maintainability and onboarding new developers. Remix promotes a unique file-system-based routing approach, which dictates some aspects of the structure. **Standard:** Structure your Remix project following a feature-centric approach, grouping related components, routes, and utilities together. **Why:** Feature-centric organization improves code discoverability, reduces cognitive load, and simplifies code reuse. It also naturally aligns with Remix's route-based data loading. **Do This:** """ my-remix-app/ ├── app/ │ ├── components/ │ │ ├── ui/ # Reusable UI components │ │ │ ├── Button.tsx │ │ │ ├── Input.tsx │ │ ├── feature/ # Components specific to a feature │ │ │ ├── ProductCard.tsx │ ├── routes/ │ │ ├── __main.tsx # Root route layout │ │ ├── index.tsx # Home page │ │ ├── products/ │ │ │ ├── $productId.tsx # Dynamic route for individual products │ │ │ ├── index.tsx # Products listing page │ │ │ ├── new.tsx # Route for creating a new product │ ├── utils/ # Utility functions and helpers │ │ ├── db.server.ts # Database connection (server-only) │ │ ├── auth.server.ts # Authentication logic (server-only) │ │ ├── api.client.ts # API client (browser-only) │ ├── entry.client.tsx # Client-side entry point │ ├── entry.server.tsx # Server-side entry point │ ├── root.tsx # Root component for the entire application │ ├── styles/ # Global styles │ │ ├── app.css │ ├── public/ │ ├── ... ├── remix.config.js ├── package.json ├── tsconfig.json """ **Don't Do This:** * Scattering related files across different directories (e.g., putting a component's styles in a separate "styles" directory *unless* those styles are truly global). * Creating overly deep directory structures. * Using generic names like "components" without further categorization. **Code Example (Component):** """tsx // app/components/feature/ProductCard.tsx import { useLoaderData } from "@remix-run/react"; import { Product } from "~/utils/types"; import styles from "./ProductCard.module.css"; interface ProductCardProps { product: Product; } export function ProductCard({ product }: ProductCardProps) { return ( <div className={styles.card}> <h3>{product.name}</h3> <p>{product.description}</p> <p>${product.price}</p> <button className={styles.button}>Add to Cart</button> </div> ); } // app/components/feature/ProductCard.module.css .card { border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; } .button { background-color: blue; color: white; border: none; padding: 5px 10px; cursor: pointer; } """ ## 2. Route-Based Data Loading and Actions Remix leverages the web's standards of HTTP for data loading and mutations. Understanding and properly utilizing loaders and actions is paramount. **Standard:** Use "loader" functions for fetching data and "action" functions for handling data mutations on your routes. **Why:** "loader" and "action" functions are the primary mechanisms for interacting with data in Remix, promoting server-side data fetching and mutations, leading to improved performance and security. This tightly integrates with Remix's routing and rendering strategy. **Do This:** """tsx // app/routes/products/$productId.tsx import { json, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"; import { useLoaderData, Form, useParams, redirect } from "@remix-run/react"; import { getProduct, updateProduct, deleteProduct } from "~/utils/db.server"; // Assuming database functions type LoaderData = { product: { id: string; name: string; description: string; price: number }; }; export const loader = async ({ params }: LoaderFunctionArgs) => { const { productId } = params; if (!productId) { throw new Error("Product ID is required"); } const product = await getProduct(productId); if (!product) { throw new Response("Not Found", { status: 404 }); } return json<LoaderData>({ product }); }; export const action = async ({ request, params }: ActionFunctionArgs ) => { const { productId } = params; if (!productId) { return json({ error: "Product ID is required" }, { status: 400 }); } const formData = await request.formData(); const intent = formData.get("_action"); if (intent === "delete") { await deleteProduct(productId); return redirect("/products"); } const updates = { name: formData.get("name") as string, description: formData.get("description") as string, price: Number(formData.get("price")), } try { await updateProduct(productId, updates); return redirect("/products/${productId}"); } catch (error: any) { return json({ errors: error }, { status: 400 }); } } export default function ProductDetails() { const { product } = useLoaderData<typeof loader>(); const { productId } = useParams(); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <p>${product.price}</p> <Form method="post"> <input type="hidden" name="_action" value="delete" /> <button type="submit">Delete</button> </Form> <Form method="post"> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" defaultValue={product.name} /><br/> <label htmlFor="description">Description:</label> <textarea id="description" name="description" defaultValue={product.description} /><br/> <label htmlFor="price">Price:</label> <input type="number" id="price" name="price" defaultValue={product.price} /><br/> <button type="submit">Update</button> </Form> </div> ); } """ **Don't Do This:** * Fetching data directly within components using "useEffect" or similar techniques (this bypasses Remix's server-side data loading benefits). * Performing data mutations directly within components (this leads to inconsistent data and security vulnerabilities). * Ignoring error handling in "loader" and "action" functions. **Anti-Pattern:** """tsx // BAD: Fetching data directly in the component import { useState, useEffect } from "react"; export default function ProductDetails() { const [product, setProduct] = useState(null); useEffect(() => { // Avoid this! fetch("/api/products/${productId}") .then((res) => res.json()) .then((data) => setProduct(data)); }, [productId]); // ... } """ ## 3. Server-Side Rendering and Progressive Enhancement Remix is built on the principles of web standards and progressive enhancement. **Standard:** Embrace server-side rendering and progressive enhancement by utilizing Remix's built-in features and avoiding client-side-only logic where possible. **Why:** Server-side rendering improves initial load times, SEO, and accessibility. Progressive enhancement ensures that the application remains functional even with JavaScript disabled or partially loaded. **Do This:** * Use Remix's "meta" function to define metadata for SEO. * Leverage server-side data fetching with "loader" functions. * Implement basic functionality using standard HTML forms and links. * Enhance the user experience with JavaScript as needed (e.g., client-side validation, animations). """tsx // app/routes/index.tsx import { MetaFunction } from "@remix-run/node"; import { Link } from "@remix-run/react"; export const meta: MetaFunction = () => { return { title: "My Awesome Remix App", description: "A demo Remix application.", }; }; export default function Index() { return ( <div> <h1>Welcome to My Remix App!</h1> <p> <Link to="/products">View Products</Link> </p> </div> ); } """ **Don't Do This:** * Relying exclusively on client-side rendering. * Using JavaScript to perform tasks that can be handled server-side. * Neglecting to provide fallback functionality for users with JavaScript disabled. ## 4. Data Modeling and Database Interactions Choose a data modeling approach that aligns with the application's requirements and utilize appropriate database interactions. **Standard:** Define clear data models and interact with the database in dedicated server-side modules using an ORM like Prisma or Drizzle, or a simpler query builder. **Why:** Separation of concerns improves code organization and testability. Using an ORM or query builder simplifies database interactions and reduces the risk of SQL injection vulnerabilities. **Do This:** """typescript // app/utils/db.server.ts import { PrismaClient } from "@prisma/client"; let prisma: PrismaClient; declare global { var __db: PrismaClient | undefined; } // Singleton to prevent hot reloading creating multiple connections if (process.env.NODE_ENV === "production") { prisma = new PrismaClient(); } else { if (!global.__db) { global.__db = new PrismaClient(); } prisma = global.__db; } export { prisma }; export async function getProduct(id: string) { return prisma.product.findUnique({ where: { id }, }); } export async function updateProduct(id: string, data: { name: string; description: string; price: number }) { return prisma.product.update({ where: { id: id }, data: data }) } export async function deleteProduct(id: string) { return prisma.product.delete({ where: { id: id } }) } """ **Don't Do This:** * Writing raw SQL queries directly in route modules. * Exposing database connection details to the client. * Over-fetching data from the database. ## 5. Session Management and Authentication Implement secure session management and authentication to protect user data. **Standard:** Use Remix's "sessionStorage" for managing user sessions and implement robust authentication and authorization mechanisms. **Why:** Secure session management and authentication are essential for protecting user data and preventing unauthorized access. "sessionStorage" provides a secure, server-side mechanism for managing session data. **Do This:** """typescript // app/utils/auth.server.ts import { createCookieSessionStorage, redirect } from "@remix-run/node"; import bcrypt from "bcryptjs"; import { prisma } from "./db.server"; const sessionSecret = process.env.SESSION_SECRET; if (!sessionSecret) { throw new Error("SESSION_SECRET must be defined"); } export const sessionStorage = createCookieSessionStorage({ cookie: { name: "my_app_session", secure: process.env.NODE_ENV === "production", secrets: [sessionSecret], sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 30, // 30 days httpOnly: true, }, }); export async function createUserSession(userId: string, redirectTo: string) { const session = await sessionStorage.getSession(); session.set("userId", userId); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session), }, }); } export async function requireUserId(request: Request, redirectTo: string = "/") { const userId = await getUserId(request); if (!userId) { throw redirect("/login?redirectTo=${redirectTo}"); } return userId; } export async function getUserId(request: Request): Promise<string | undefined> { const session = await sessionStorage.getSession(request.headers.get("Cookie")); return session.get("userId"); } export async function getUser(request: Request) { const userId = await getUserId(request); if (!userId) { return null; } const user = await prisma.user.findUnique({ where: { id: userId }, }); return user; } export async function logout(request: Request) { const session = await sessionStorage.getSession( request.headers.get("Cookie") ); return redirect("/", { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, }); } export async function register({ email, password } : any) { const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { return json({ error: "User already exists with that email" }, { status: 400 }) } const hashedPassword = await bcrypt.hash(password, 10); try { const user = await prisma.user.create({ data: { email, password: hashedPassword, }, }); return createUserSession(user.id, "/"); } catch (e : any) { return json({ error: "Something bad happened: ${e.message}", }, { status: 400 }); } } export async function login({ email, password } : any) { const user = await prisma.user.findUnique({ where: { email }, }); if (!user) { return json({ error: "Incorrect email or password" }, { status: 400 }) } const isCorrectPassword = await bcrypt.compare(password, user.password); if (!isCorrectPassword) { return json({ error: "Incorrect email or password" }, { status: 400 }) } return createUserSession(user.id, "/"); } // Example Usage in a Route (requiring user ID): // app/routes/protected.tsx import { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { requireUserId, getUser } from "~/utils/auth.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await requireUserId(request); // Redirects to login if not authenticated const user = await getUser(request); return { user }; }; export default function ProtectedRoute() { const { user } = useLoaderData<typeof loader>(); return ( <div> <h1>Welcome, {user?.email}!</h1> {/* ... protected content ... */} </div> ); } """ **Don't Do This:** * Storing sensitive data in cookies without proper encryption. * Implementing authentication logic on the client-side. * Using weak passwords or insecure hashing algorithms. ## 6. Error Handling and Logging Implement comprehensive error handling and logging to identify and address issues quickly. **Standard:** Implement global error boundaries and logging mechanisms to capture and report errors effectively. **Why:** Robust error handling and logging are crucial for maintaining application stability and identifying potential problems. **Do This:** * Use "ErrorBoundary" components to catch errors that occur during rendering. * Implement server-side logging to track errors and exceptions. * Use a centralized logging service like Sentry or Bugsnag. * Provide informative error messages to users without exposing sensitive information. """tsx // app/root.tsx import { Links, Meta, Outlet, Scripts, LiveReload, useCatch, } from "@remix-run/react"; import type { MetaFunction, LinksFunction, } from "@remix-run/node"; import appStylesHref from "./styles/app.css"; export const meta: MetaFunction = () => ({ charset: "utf-8", title: "Remix App", viewport: "width=device-width,initial-scale=1", }); export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: appStylesHref }]; }; export default function App() { return ( <html lang="en"> <head> <Meta /> <Links /> </head> <body> <Outlet /> <Scripts /> <LiveReload /> </body> </html> ); } export function ErrorBoundary({ error }: { error: Error }) { console.error(error); return ( <html> <head> <Meta /> <Links /> <title>Oh no!</title> </head> <body> <h1>App Error</h1> <p>{error.message}</p> <p>The stack trace is:</p> <pre>{error.stack}</pre> </body> </html> ); } export function CatchBoundary() { const caught = useCatch(); const params = useCatch(); return ( <html> <head> <Meta /> <Links /> <title>Oh no!</title> </head> <body> <h1> {caught.status} {caught.statusText} </h1> </body> </html> ); } """ **Don't Do This:** * Relying solely on client-side error handling. * Logging sensitive information (e.g., passwords) in plain text. * Ignoring errors or allowing them to crash the application silently. ## 7. Environment Variables and Configuration Manage environment variables and configuration settings securely. **Standard:** Use environment variables for sensitive configuration settings and store them securely using tools like Doppler or Fly.io secrets. **Why:** Environment variables allow you to configure the application without modifying code and prevent sensitive information from being committed to source control. **Do This:** * Store sensitive data (e.g., API keys, database passwords) in environment variables. * Use a ".env" file for local development. * Use a tool like Doppler or Fly.io secrets for production environment variables. * Validate environment variables at application startup. """typescript // Example: Accessing an environment variable (server-side only!) // Ensure SESSION_SECRET is defined const sessionSecret = process.env.SESSION_SECRET; if (!sessionSecret) { throw new Error("SESSION_SECRET must be defined"); } """ **Don't Do This:** * Committing sensitive information to source control. * Hardcoding configuration settings in the code. * Exposing environment variables to the client. ## 8. Dependencies and Versioning Manage project dependencies and versions effectively. **Standard:** Use a package manager like npm or yarn to manage project dependencies and specify version ranges using semantic versioning. **Why:** Dependency management ensures that the application uses compatible versions of its dependencies and simplifies updating to newer versions. **Do This:** * Use "npm install" or "yarn add" to install dependencies. * Specify version ranges in "package.json" using semantic versioning (e.g., "^1.2.3", "~2.0.0"). * Use "npm audit" or "yarn audit" to identify and fix security vulnerabilities in dependencies. * Regularly update dependencies to the latest versions. **Don't Do This:** * Installing dependencies globally. * Using wildcard version ranges (e.g., "*"). * Ignoring security vulnerabilities in dependencies. ## 9. API Design When building APIs, even internal ones for Remix's data loading and actions, follow RESTful principles where appropriate. **Standard:** Design APIs following RESTful principles, where applicable. Utilize HTTP methods correctly (GET, POST, PUT, DELETE) and provide clear, consistent endpoints. **Why:** RESTful APIs are well-defined, easy to understand, scale, and maintain. Following these principles promotes interoperability. **Do This:** * Use the correct HTTP method for each operation (e.g., GET for retrieving data, POST for creating data, PUT for updating data, DELETE for deleting data). * Use meaningful endpoint names (e.g., "/products", "/products/:id"). * Return appropriate HTTP status codes (e.g., 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error). * Use JSON for request and response bodies. **Don't Do This:** * Using GET requests for data mutations. * Using inconsistent endpoint naming conventions. * Returning unclear or ambiguous error messages. ## 10. Testing Implement comprehensive testing strategies. **Standard:** Write unit, integration, and end-to-end tests to ensure application reliability. **Why:** Testing catches bugs early, reduces regressions, and facilitates confident code changes. **Do This:** * Write unit tests for individual components and functions. * Write integration tests to verify interactions between different parts of the application. * Write end-to-end tests to simulate user interactions and verify the application's overall functionality. * Use a testing framework like Jest or Mocha. * Use a testing library like React Testing Library for component testing. * Use Playwright or Cypress for end-to-end testing. * Implement Continuous Integration (CI) to automatically run tests on every commit. **Don't Do This:** * Skipping tests due to time constraints. * Writing tests that are tightly coupled to implementation details. * Ignoring failing tests.
# Component Design Standards for Remix This document outlines the coding standards for designing and building reusable and maintainable components in Remix applications. These standards aim to leverage Remix's unique capabilities while adhering to modern React best practices. ## 1. General Principles ### 1.1. Reusability **Standard:** Design components to be reusable across multiple contexts. * **Do This:** Parameterize components with props to control their behavior and appearance. * **Don't Do This:** Embed hardcoded values or application-specific logic within components that prevent them from being used in other parts of the application. **Why:** Reusable components reduce code duplication, improve maintainability, and ensure consistency across the application. **Example:** """jsx // Good: Reusable Button Component export function Button({ children, onClick, variant = "primary" }: { children: React.ReactNode; onClick: () => void; variant?: "primary" | "secondary" }) { const className = "button ${variant}"; return ( <button className={className} onClick={onClick}> {children} </button> ); } // Usage Example function MyComponent() { return ( <div> <Button onClick={() => console.log("Clicked!")}>Click Me</Button> <Button variant="secondary" onClick={() => console.log("Clicked again!")}>Another Button</Button> </div> ); } """ """jsx // Bad: Hardcoded Button function HardcodedButton() { return ( <button className="specific-button" onClick={() => console.log("Clicked!")}> Click This Specific Button </button> ); } """ ### 1.2. Single Responsibility Principle **Standard:** Each component should have one specific responsibility. * **Do This:** Break down complex UIs into smaller, focused components. * **Don't Do This:** Create "god components" that handle multiple unrelated tasks. **Why:** Components with a single responsibility are easier to understand, test, and modify. **Example:** """jsx // Good: Separate concern components // Display Component function Display({ value }: { value: number }) { return <div>{value}</div>; } // Button Component function IncrementButton({ onClick }: { onClick: () => void }) { return <button onClick={onClick}>Increment</button>; } // Counter Component orchestrates the other components function Counter() { const [count, setCount] = React.useState(0); const increment = () => { setCount(count + 1); }; return ( <div> <Display value={count} /> <IncrementButton onClick={increment} /> </div> ); } """ """jsx // Bad: All-in-one component function ComplexCounter() { const [count, setCount] = React.useState(0); const increment = () => { setCount(count + 1); }; return ( <div> <div>{count}</div> <button onClick={increment}>Increment</button> </div> ); } """ ### 1.3. Composition **Standard:** Build complex UIs by composing smaller, simpler components. * **Do This:** Favor composition over inheritance. Use props.children to allow flexibility. * **Don't Do This:** Rely on deeply nested component hierarchies with tight coupling. **Why:** Composition promotes modularity and allows for greater flexibility when assembling user interfaces. It allows parent components to inject content and behavior into their children. **Example:** """jsx // Good: Utilizing children prop function Card({ children }: { children: React.ReactNode }) { return ( <div className="card"> {children} </div> ); } function App() { return ( <Card> <h2>Welcome</h2> <p>This is a card component.</p> </Card> ); } """ ### 1.4. Separation of Concerns (Data Fetching and Presentation) **Standard:** Keep data fetching logic separate from the presentation logic within components. * **Do This:** Fetch data in Remix loaders and pass it as props to presentational components. Use action functions for Mutations, avoiding direct API calls inside components. * **Don't Do This:** Perform data fetching directly within components using "useEffect" (unless it’s client-side specific). **Why:** This separation improves testability, maintainability, and performance. Remix handles data fetching on the server, providing benefits like improved SEO and faster initial load times. Managing server-side and client-side concerns separately makes the code easier to reason about. **Example:** """jsx // app/routes/my-route.tsx import { LoaderFunctionArgs, json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; // Loader function (runs on the server) export const loader = async ({ request }: LoaderFunctionArgs) => { const data = await fetchDataFromAPI(); // Replace with your actual data fetching logic return json({ data }); }; // Presentational Component function MyComponent({ data }: { data: any }) { return ( <div> {/* Render data here */} {data.map((item: any) => ( <div key={item.id}>{item.name}</div> ))} </div> ); } // Route Component export default function MyRoute() { const { data } = useLoaderData<typeof loader>(); return <MyComponent data={data} />; } async function fetchDataFromAPI() { // Simulate an API call return new Promise((resolve) => { setTimeout(() => { resolve([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]); }, 500); }); } """ ### 1.5. Controlled vs. Uncontrolled Components **Standard:** Understand and strategically use controlled and uncontrolled components based on the situation. * **Controlled Components:** Data is controlled by the parent component via props and callbacks. The source of truth resides outside the component. Favor controlled components when you need fine-grained control over user input, validation, or integration with other parts of the application state. * **Uncontrolled Components:** Data is handled internally within the component, typically using refs to access the DOM. Simplier for basic use cases, but harder to integrate with application-wide state management. **Why:** Choosing the right approach improves component behavior, data management, and overall app architecture. Remix benefits from the predictable data flow via loaders and allows the flexibility of client-side behavior when needed. **Example (Controlled Component with Remix Form):** """jsx // app/routes/form-example.tsx import { Form, useActionData, useNavigation } from "@remix-run/react"; import { ActionFunctionArgs, json } from "@remix-run/node"; export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const name = formData.get("name") as string; if (!name) { return json({ errors: { name: "Name is required" } }, { status: 400 }); } // Process the form data (e.g., save to database) console.log("Name submitted:", name); return json({ success: true, name }); }; export default function FormExample() { const actionData = useActionData<typeof action>(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; return ( <Form method="post"> <label> Name: <input type="text" name="name" defaultValue={actionData?.name} /> </label> {actionData?.errors?.name && ( <div style={{ color: "red" }}>{actionData.errors.name}</div> )} <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Submitting..." : "Submit"} </button> {actionData?.success && <div>Successfully submitted: {actionData.name}</div>} </Form> ); } """ **Example (Uncontrolled Component):** """jsx function UncontrolledInput() { const inputRef = React.useRef<HTMLInputElement>(null); const handleSubmit = () => { if (inputRef.current) { console.log("Input Value:", inputRef.current.value); } }; return ( <div> <input type="text" ref={inputRef} /> <button onClick={handleSubmit}>Get Value</button> </div> ); } """ ## 2. Component Structure and Organization ### 2.1. File Structure **Standard:** Organize components into a clear and consistent file structure. * **Do This:** Group related components together in directories. Use a consistent naming convention for files and directories (e.g., "ComponentName.tsx", "components/ComponentName/"). * **Don't Do This:** Scatter component files randomly throughout the project. **Why:** Consistent file structure improves code discoverability and maintainability. **Example:** """ src/ ├── components/ │ ├── Button/ │ │ ├── Button.tsx // The main component │ │ ├── Button.css // Component-specific styles │ │ ├── Button.test.tsx // Unit tests for the component │ │ └── index.ts // (Optional) Exports the component │ ├── Card/ │ │ ├── Card.tsx │ │ └── Card.css │ └── ... """ ### 2.2. Component Exports **Standard:** Use explicit exports for components. * **Do This:** Use named exports for components. Create an "index.ts" file to re-export components for easier imports. * **Don't Do This:** Use default exports excessively. **Why:** Named exports provide better code clarity and allow for easier refactoring. Re-exporting from "index.ts" offers a simplified import path into other portions of the code. **Example:** """jsx // components/Button/Button.tsx export function Button({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { return ( <button onClick={onClick}> {children} </button> ); } // components/Button/index.ts export { Button } from "./Button"; // Usage: import { Button } from "~/components/Button"; //This assumes your src directory is mapped to the alias ~ """ ### 2.3. Prop Types **Standard:** Define clear and explicit prop types for all components. * **Do This:** Use TypeScript interfaces or types to define prop shapes. Employ "zod" for runtime validation and schema definition that can be used to define data expected from loaders / actions. * **Don't Do This:** Use "any" for prop types or omit prop types altogether. **Why:** Prop types improve code safety, readability, and maintainability. They also help catch errors early in the development process. **Example (TypeScript):** """tsx interface ButtonProps { children: React.ReactNode; onClick: () => void; variant?: "primary" | "secondary"; } export function Button({ children, onClick, variant = "primary" }: ButtonProps) { const className = "button ${variant}"; return ( <button className={className} onClick={onClick}> {children} </button> ); } """ **Example (Zod):** """ts import { z } from "zod"; const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); type User = z.infer<typeof UserSchema>; // Example loader function (app/routes/users.tsx) export const loader = async () => { const userData = { id: 1, name: "John Doe", email: "john.doe@example.com" }; // Validate data against the schema const parsedData = UserSchema.parse(userData); // Will throw an error if the data doesn't match the schema return json<User>(parsedData); }; """ ## 3. Component Implementation ### 3.1. State Management **Standard:** Choose the appropriate state management solution based on the complexity of the component and the application. * **Do This:** Use "useState" for local component state. Use "useContext" for simple global state. Use a dedicated state management library (e.g., Zustand, Jotai, Redux Toolkit) for complex application state. Leverage Remix's loaders/actions strategically. * **Don't Do This:** Overuse global state for local component state. Maintain a clear separation between client-side and server-side state. **Why:** Proper state management improves component performance, predictability, and maintainability. **Example (useState):** """jsx function Counter() { const [count, setCount] = React.useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } """ **Example (useContext):** """jsx // Create a context const ThemeContext = React.createContext({ theme: "light", toggleTheme: () => {} }); // Provider component function ThemeProvider({ children, initialTheme }: { children: React.ReactNode; initialTheme: string }) { const [theme, setTheme] = React.useState(initialTheme); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); }; const value = { theme, toggleTheme }; return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; } // Consumer component function ThemedButton() { const { theme, toggleTheme } = React.useContext(ThemeContext); return <button onClick={toggleTheme}>Toggle Theme ({theme})</button>; } // Usage function App() { return ( <ThemeProvider initialTheme="light"> <ThemedButton /> </ThemeProvider> ); } """ ### 3.2. Event Handling **Standard:** Handle events correctly and efficiently. * **Do This:** Use arrow functions to bind event handlers to the component instance. Debounce or throttle event handlers for performance-sensitive events. * **Don't Do This:** Create unnecessary closures or inline functions within render methods. This can lead to performance issues due to re-renders. **Why:** Proper event handling improves component performance and responsiveness. **Example:** """jsx function MyComponent() { const [inputValue, setInputValue] = React.useState(""); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInputValue(event.target.value); }; return ( <input type="text" value={inputValue} onChange={handleChange} /> ); } """ ### 3.3. Asynchronous Operations **Standard:** Manage asynchronous operations carefully to prevent race conditions and ensure UI consistency. * **Do This:** Use Remix action functions properly for server-side data mutations. When client-side asynchronous operations are required, manage them using "async/await" and handle errors gracefully. Avoid "fire and forget" async calls. Utilize "useTransition" hook for providing user feedback in the UI. * **Don't Do This:** Neglect error handling in asynchronous operations. Perform unnecessary or redundant data fetching. Mutate state directly without considering potential race conditions. **Why:** Correctly managing asynchronous operations ensures data integrity and a smooth user experience. Remix allows for a better separation of data mutations compared to a client-side only framework. **Example (Remix Action with "useTransition"):** """jsx // app/routes/my-route.tsx import { Form, useTransition } from "@remix-run/react"; import { ActionFunctionArgs, json } from "@remix-run/node"; export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const inputValue = formData.get("myInput") as string; try { // Simulate a database save with a delay await new Promise(resolve => setTimeout(resolve, 1000)); // Successful save return json({ success: true, message: "Saved: ${inputValue}" }); } catch (error) { console.error("Save failed:", error); return json({ success: false, message: "Save failed" }, { status: 500 }); } }; export default function MyRoute() { const transition = useTransition(); // Tracks the state of the action const isSubmitting = transition.state === "submitting"; return ( <Form method="post"> <input type="text" name="myInput" disabled={isSubmitting} /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Saving..." : "Save"} </button> {transition.submission && ( <div>Saving with value: {transition.submission.formData.get("myInput")}</div> // Show current form data )} {transition.state === "idle" && transition.submission && transition.submission.state === "finished" && ( <div>Successfully Saved!</div> // Show success message after submission )} {transition.error && ( <div style={{ color: "red" }}>Save failed. Please try again.</div> // Show error if applicable )} </Form> ); } """ ### 3.4. Styling **Standard:** Choose a consistent styling approach for your components. * **Do This:** Use CSS Modules, Styled Components, Tailwind CSS, or another established styling solution. Follow BEM or a similar naming convention for CSS classes. * **Don't Do This:** Use inline styles excessively. Mix different styling approaches within the same project. **Why:** Consistent styling improves code readability and maintainability. **Example (CSS Modules):** """jsx // Component.module.css .container { background-color: #f0f0f0; padding: 16px; } .title { font-size: 20px; font-weight: bold; } """ """jsx // Component.tsx import styles from "./Component.module.css"; function Component() { return ( <div className={styles.container}> <h2 className={styles.title}>My Component</h2> <p>This is a styled component.</p> </div> ); } """ **Example (Tailwind CSS):** """jsx function Button({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { return ( <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={onClick}> {children} </button> ); } """ ## 4. Remix-Specific Considerations ### 4.1. Data Loading and Actions **Standard:** Leverage Remix's data loading and action capabilities to fetch data on the server and mutate data efficiently. * **Do This:** Use "loader" functions to fetch data for routes and pass the data as props to components. Use "action" functions to handle form submissions and data mutations. * **Don't Do This:** Perform data fetching directly within components using "useEffect" (except for client-side data requirements). Perform data mutations directly within components. **Why:** Remix's data loading and action capabilities provide significant performance and SEO benefits. They also promote a clear separation of concerns between data fetching and presentation. ### 4.2. Error Handling in Loaders and Actions **Standard:** Implement robust error handling in Remix loaders and actions. * **Do This:** Use "try...catch" blocks to catch errors in loaders and actions. Return "Response" objects with appropriate error codes to signal errors to the client. * **Don't Do This:** Allow errors to bubble up unhandled. Neglect to provide user-friendly error messages. **Why:** Proper error handling ensures that the application degrades gracefully in the face of errors. **Example:** """jsx // app/routes/my-route.tsx import { LoaderFunctionArgs, json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; interface Data { items: string[] } export const loader = async ({ request }: LoaderFunctionArgs) => { try { const data = await fetchDataFromAPI(); return json<Data>({ items: data }); } catch (error: any) { console.error("Data fetching error:", error); throw new Response("Failed to fetch data", { status: 500 }); //Uses Remix's thrown Responses for Error Boundaries } }; function MyComponent() { const { items } = useLoaderData<typeof loader>(); return ( <div> {items.map((item: string) => ( <div key={item}>{item}</div> ))} </div> ); } export default function MyRoute() { return ( <React.Suspense fallback={<p>Loading...</p>}> <MyComponent /> </React.Suspense> ) } async function fetchDataFromAPI() { return new Promise<string[]>((resolve, reject) => { setTimeout(() => { const shouldFail = Math.random() < 0.5; if (shouldFail) { reject(new Error("Simulated API Error")); } else { resolve(["Item 1", "Item 2", "Item 3"]); } }, 500); }); } """ ### 4.3. Optimistic UI Updates **Standard**: Understand and properly implement Optimistic UI updates to improve perceived performance. * **Do This**: Optimistically update the UI before the server responds to an action. This involves immediately reflecting the user's action in the UI, assuming the action will be successful. Revert changes if the action fails. Use Libraries such as "use-optimistic-mutation" for easier management. * **Don't Do This**: Neglect to handle errors gracefully when optimistic updates fail. Design optimistic updates that significantly deviate from the actual server-side outcome. **Why**: Optimistic UI makes the application feel faster and more responsive, providing a better user experience. """jsx import { useOptimisticMutation, type OptimisticMutationHelpers } from 'use-optimistic-mutation'; import { useState } from 'react'; interface Task { id: number; title: string; completed: boolean; } async function updateTask(task: Task): Promise<Task> { await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency return { ...task, completed: !task.completed }; } export default function TaskList() { const [tasks, setTasks] = useState<Task[]>([ { id: 1, title: 'Learn Remix', completed: false }, { id: 2, title: 'Build something awesome', completed: false }, ]); const [mutate, { isMutating }] = useOptimisticMutation( async (task: Task) => { const updatedTask = await updateTask(task); return updatedTask; }, { optimisticUpdate(task: Task) { return { value: tasks.map(t => (t.id === task.id ? { ...t, completed: !t.completed } : t)), revert(originalValue: Task[]) { setTasks(originalValue) }, }; }, } ); const handleToggle = async (task: Task) => { const originalValue = tasks setTasks(tasks.map(t => (t.id === task.id ? { ...t, completed: !t.completed } : t))); //Optimistic Update try { await mutate(task); } catch (e) { //Revert on Error setTasks(originalValue) } }; return ( <ul> {tasks.map(task => ( <li key={task.id}> <label> <input type="checkbox" checked={task.completed} onChange={() => handleToggle(task)} disabled={isMutating} /> {task.title} </label> </li> ))} </ul> ); } """ These component design standards for Remix provide a solid foundation for building high-quality, maintainable, and performant applications. By adhering to these guidelines, developers can leverage the power of Remix while ensuring consistency and efficiency in their codebase.
# 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.
# 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.