# API Integration Standards for Solid.js
This document outlines the coding standards for API integration in Solid.js applications. Adhering to these standards ensures maintainability, performance, security, and a consistent codebase.
## 1. Architectural Patterns for API Integration
Choosing the right architectural pattern is crucial for managing complexity and ensuring scalability.
### 1.1 Abstraction Layers
Employ abstraction layers to decouple components from specific API implementations. This makes it easier to switch or update APIs without affecting the rest of the application.
**Do This:**
* Create a service layer or a repository pattern.
* Define clear interfaces/types for API interactions.
**Don't Do This:**
* Directly call "fetch" or "axios" in components.
* Hardcode API endpoints within components.
**Why:** Decoupling reduces dependencies and simplifies testing and future modifications.
**Code Example:**
"""typescript
// src/services/productService.ts
import { createResource } from 'solid-js';
export interface Product {
id: number;
name: string;
price: number;
}
const fetchProducts = async (): Promise => {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
};
export function useProducts() {
const [products] = createResource(fetchProducts);
return products;
}
"""
"""typescript
// src/components/ProductList.tsx
import { useProducts } from '../services/productService';
import { Show } from 'solid-js';
function ProductList() {
const products = useProducts();
return (
Products
Loading...<p></p>}>
{(products) => (
{products.map((product) => (
{product.name} - ${product.price}
))}
)}
);
}
export default ProductList;
"""
### 1.2 Centralized API Configuration
Store API endpoints, headers, and other configurations in a central location (e.g., a config file or environment variables).
**Do This:**
* Use environment variables for sensitive information like API keys.
* Define base URLs centrally.
**Don't Do This:**
* Hardcode API keys directly in the code.
* Repeat API configuration in multiple places.
**Why:** Centralized configuration makes it easier to manage and update API settings across the application. It also improves security by keeping sensitive information out of the codebase.
**Code Example:**
"""typescript
// config.ts
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
export const API_KEY = import.meta.env.VITE_API_KEY;
if (!API_BASE_URL) {
console.warn("VITE_API_BASE_URL is not set. Using default: http://localhost:3000");
}
if (!API_KEY) {
console.warn("VITE_API_KEY is not set. Ensure the API key is set in your environment for production use.");
}
"""
"""typescript
// src/services/productService.ts
import { createResource } from 'solid-js';
import { API_BASE_URL } from '../config';
export interface Product {
id: number;
name: string;
price: number;
}
const fetchProducts = async (): Promise => {
const response = await fetch("${API_BASE_URL}/api/products");
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
};
export function useProducts() {
const [products] = createResource(fetchProducts);
return products;
}
"""
## 2. Data Fetching with Solid.js
Solid.js provides several tools for efficient and reactive data fetching.
### 2.1 "createResource" Hook
Utilize "createResource" hook for asynchronous data fetching. It seamlessly integrates with Solid's reactivity system.
**Do This:**
* Use "createResource" for reading data from an API.
* Handle loading, error, and data states appropriately.
**Don't Do This:**
* Mutate data directly inside the "createResource"'s fetcher function.
* Ignore the error state returned by "createResource".
**Why:** "createResource" leverages Solid.js's reactivity to efficiently manage asynchronous data and provides built-in mechanisms for handling loading states and errors.
**Code Example:**
"""typescript
import { createResource, createSignal, onMount } from 'solid-js';
interface Post {
id: number;
title: string;
body: string;
}
const fetchPost = async (id: number): Promise => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/${id}");
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
};
function PostComponent() {
const [postId, setPostId] = createSignal(1);
const [post, { mutate, refetch }] = createResource(postId, fetchPost);
onMount(() => {
// Example: Refetch data after 5 seconds
setTimeout(() => refetch(), 5000);
});
const changePostId = (newId: number) => {
setPostId(newId);
};
return (
changePostId(postId() + 1)}>Load Next Post
Loading...<p></p>}>
{(post) => (
{post.title}
<p>{post.body}</p>
)}
{(error) => <p>Error: {error.message}</p>}
);
}
export default PostComponent;
"""
### 2.2 Optimistic Updates
Use optimistic updates (via the "mutate" function provided by "createResource") to improve perceived performance.
**Do This:**
* Update the UI optimistically before submitting the API request.
* Handle potential errors and revert the UI if the request fails.
**Don't Do This:**
* Assume all API requests will succeed.
* Forget to handle revert in case of failure with optimistic updates.
**Why:** Optimistic updates can significantly improve the user experience by making the UI feel more responsive.
**Code Example:**
"""typescript
import { createResource, createSignal } from 'solid-js';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const fetchTodos = async (): Promise => {
const response = await fetch('/api/todos');
return await response.json();
};
const updateTodo = async (id: number, completed: boolean): Promise => {
const response = await fetch("/api/todos/${id}", {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ completed }),
});
return await response.json();
};
function TodoList() {
const [todos, { mutate }] = createResource(fetchTodos);
const toggleTodo = async (todo: Todo) => {
const originalCompleted = todo.completed;
// Optimistic update
mutate((prevTodos) => {
if(!prevTodos) return prevTodos;
return prevTodos.map((t) => (t.id === todo.id ? { ...t, completed: !t.completed } : t));
});
try {
await updateTodo(todo.id, !todo.completed);
} catch (error) {
console.error("Failed to update todo on the server:", error);
// Revert the optimistic update on error
mutate((prevTodos) => {
if(!prevTodos) return prevTodos;
return prevTodos.map((t) => (t.id === todo.id ? { ...t, completed: originalCompleted } : t));
});
}
};
return (
Loading...<p></p>}>
{(todos) => (
todos.map((todo) => (
toggleTodo(todo)}
/>
{todo.text}
))
)}
);
}
export default TodoList;
"""
### 2.3 Server-Side Rendering (SSR) and Prefetching
Optimize for SSR by prefetching data on the server and passing it to the client.
**Do This:**
* Use a framework like "solid-start" for SSR.
* Prefetch initial data to minimize client-side fetching.
**Don't Do This:**
* Perform all data fetching on the client when using SSR.
**Why:** SSR improves initial load times and SEO by rendering the page on the server.
**Code Example (SolidStart):**
"""typescript
// src/routes/index.tsx
import { createResource } from 'solid-js';
import { API_BASE_URL } from '~/config';
import type { RouteDataFunc } from "solid-start";
import { fetch } from "solid-start/server";
interface Post {
id: number;
title: string;
body: string;
}
export const routeData: RouteDataFunc = async () => {
const fetchPosts = async (): Promise => {
const response = await fetch("${API_BASE_URL}/posts"); // Assume your API is at this route
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
};
return {
posts: fetchPosts(),
};
};
export default function Index() {
const posts = routeData(() => {});
return (
Posts
Loading posts...<p></p>}>
{(posts) => (
{posts.map((post) => (
{post.title}
<p>{post.body}</p>
))}
)}
);
}
"""
## 3. API Request Handling
Implement well-defined strategies for making API requests.
### 3.1 Using "fetch" API
Use the built-in "fetch" API or a library like "axios". Ensure proper error handling and request configuration.
**Do This:**
* Use "async/await" syntax for cleaner code.
* Set appropriate headers for content type and authorization.
* Handle network errors and API errors separately.
**Don't Do This:**
* Ignore potential errors from "fetch".
* Use deprecated "XMLHttpRequest".
**Why:** "fetch" is the modern standard for making HTTP requests in JavaScript environments. The "async/await" syntax makes asynchronous code easier to read and manage. Proper error handling is crucial for a reliable application.
**Code Example:**
"""typescript
const createPost = async (data: { title: string; body: string }): Promise => {
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer ${API_KEY}"
},
body: JSON.stringify(data),
});
if (!response.ok) {
// Handle HTTP errors based on status code
if (response.status === 400) {
throw new Error("Bad Request: Invalid data");
} else if (response.status === 401) {
throw new Error("Unauthorized: Invalid API key or no permission");
} else {
throw new Error("HTTP error! Status: ${response.status}");
}
}
return await response.json();
} catch (error: any) {
// Handle network errors (e.g., no internet connection)
console.error("Network error or API error:", error.message);
throw error; // Re-throw the error to be caught by the component
}
};
"""
### 3.2 Request Cancellation
Implement request cancellation mechanisms to prevent unnecessary network requests and improve performance. This is especially useful for auto-saving, filtering, or scenarios when the user quickly navigates away from a page.
**Do This:**
* Use "AbortController" to cancel pending "fetch" requests.
**Don't Do This:**
* Leave requests hanging when they are no longer needed.
**Why:** Cancelling unnecessary requests prevents wasted bandwidth and processing power, leading to better performance and a smoother user experience.
**Code Example:**
"""typescript
import { createSignal, onCleanup } from 'solid-js';
const useFetchWithCancellation = () => {
const [data, setData] = createSignal(null);
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal(null);
const [controller, setController] = createSignal(new AbortController());
const fetchData = async (url: string) => {
setLoading(true);
setError(null);
const newController = new AbortController();
setController(newController);
try {
const response = await fetch(url, { signal: newController.signal });
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const result = await response.json();
setData(result);
} catch (e: any) {
if (e.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(e);
}
} finally {
setLoading(false);
}
};
const cancelFetch = () => {
controller().abort();
};
onCleanup(cancelFetch); // Cancel fetch when the component unmounts
return { data, loading, error, fetchData, cancelFetch };
};
//Example Usage
function MyComponent() {
const { data, loading, error, fetchData, cancelFetch } = useFetchWithCancellation();
const [url, setUrl] = createSignal("/api/data");
const loadData = () => {
fetchData(url());
}
return (
Load Data
Cancel Fetch
{loading() && <p>Loading...</p>}
{error() && <p>Error: {error()?.message}</p>}
{data() && <pre>{JSON.stringify(data(), null, 2)}</pre>}
);
}
export default MyComponent;
"""
## 4. Data Transformation and Validation
Ensure data received from APIs is transformed and validated before being used in the UI.
### 4.1 Data Mapping
Map API responses to a consistent data structure used by the application.
**Do This:**
* Create dedicated mapping functions.
* Handle different API versions or response formats gracefully.
**Don't Do This:**
* Directly use raw API data in the UI without transformation.
**Why:** Data mapping ensures consistency and reduces the impact of API changes on the application's codebase.
**Code Example:**
"""typescript
// src/utils/dataMapping.ts
import { Product } from '../services/productService';
interface ApiResponseProduct {
productId: number;
productName: string;
productPrice: number;
}
export const mapApiResponseToProduct = (apiResponse: ApiResponseProduct): Product => {
return {
id: apiResponse.productId,
name: apiResponse.productName,
price: apiResponse.productPrice,
};
};
export const mapApiResponseProducts = (apiResponses: ApiResponseProduct[]): Product[] => {
return apiResponses.map(mapApiResponseToProduct);
};
"""
"""typescript
// src/services/productService.ts
import { createResource } from 'solid-js';
import { mapApiResponseProducts } from '../utils/dataMapping';
export interface Product {
id: number;
name: string;
price: number;
}
const fetchProducts = async (): Promise => {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const apiResponse = await response.json();
return mapApiResponseProducts(apiResponse); // Mapping here
};
export function useProducts() {
const [products] = createResource(fetchProducts);
return products;
}
"""
### 4.2 Data Validation
Validate data received from APIs using libraries like "zod" or "yup" to ensure data integrity.
**Do This:**
* Define schemas for API responses.
* Handle validation errors appropriately.
**Don't Do This:**
* Assume API data is always correct.
* Ignore validation errors, causing unexpected UI behavior.
**Why:** Data validation prevents unexpected errors and ensures that the application only processes valid data.
**Code Example (Zod):**
"""typescript
import { z } from 'zod';
// Define a Zod schema for a Post
const PostSchema = z.object({
id: z.number(),
title: z.string(),
body: z.string(),
userId: z.number(),
});
// Define a TypeScript type from the Zod schema
type Post = z.infer;
const fetchPost = async (id: number): Promise => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/${id}");
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const data = await response.json();
// Validate the data against the schema
const result = PostSchema.safeParse(data);
if (!result.success) {
console.error("Validation error:", result.error.issues);
throw new Error("Invalid post data received from API");
}
return result.data;
};
"""
## 5. Error Handling
Implement robust error-handling strategies to gracefully manage API errors and network issues.
### 5.1 Centralized Error Handling
Use a centralized error handling mechanism to log and display errors consistently.
**Do This:**
* Create an error boundary component to catch errors. Solid does not have one by default like React. Instead use a "try...catch" block on main routes, or implement your own at the component level manually.
* Log errors to a monitoring service (e.g., Sentry, LogRocket).
**Don't Do This:**
* Ignore errors or handle them inconsistently across the application.
* Expose raw error messages to the user without sanitization.
**Why:** Centralized error handling simplifies error management and provides a consistent user experience. Logging errors allows for proactive monitoring and debugging.
**Code Example (simple error boundary):**
"""typescript
import { createSignal } from 'solid-js';
function ErrorBoundary(props: { children: any }) {
const [error, setError] = createSignal(null);
const handleError = (e: any) => {
console.error("Caught an error:", e);
setError(e);
};
if (error()) {
return (
Oops! Something went wrong.
<p>{error()?.message || 'An unexpected error occurred.'}</p>
{/* Optionally, add a button to reset the error and retry */}
);
}
try {
return props.children;
} catch (e: any) {
handleError(e);
return (
Oops! Something went wrong.
<p>{e?.message || 'An unexpected error occurred.'}</p>
);
}
}
function MyComponentThatMightFail() {
//... possible error condition example
if (Math.random() > 0.5) {
throw new Error("This component failed randomly!");
}
return <p>This component rendered successfully.</p>;
}
function App() {
return (
);
}
export default App;
"""
### 5.2 Retry Mechanism
Implement a retry mechanism for transient API errors.
**Do This:**
* Use exponential backoff to avoid overwhelming the API.
* Limit the number of retries.
**Don't Do This:**
* Retry indefinitely without a limit.
* Retry immediately after an error without a delay.
**Why:** A retry mechanism improves the resilience of the application by automatically recovering from temporary network issues or API downtime.
**Code Example:**
"""typescript
const fetchWithRetry = async (url: string, options: RequestInit = {}, maxRetries = 3, delay = 1000): Promise => {
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Check if the error is retryable (e.g., 5xx status codes)
if (response.status >= 500 && response.status < 600) {
retryCount++;
console.log("Attempt ${retryCount} failed with status ${response.status}. Retrying in ${delay}ms...");
await new Promise(resolve => setTimeout(resolve, delay * retryCount)); // Exponential backoff
} else {
// Non-retryable error, re-throw the error
throw new Error("Request failed with status ${response.status}");
}
} catch (error) {
retryCount++;
console.error("Attempt ${retryCount} failed with error: ${error}. Retrying in ${delay}ms...");
await new Promise(resolve => setTimeout(resolve, delay * retryCount));
}
}
throw new Error("Request failed after ${maxRetries} retries");
};
// Example usage:
const fetchData = async () => {
try {
const response = await fetchWithRetry('/api/data');
const data = await response.json();
console.log('Data:', data);
} catch (error) {
console.error('Failed to fetch data after multiple retries:', error);
}
};
"""
## 6. Security Considerations
Implement security best practices when interacting with APIs.
### 6.1 Input Sanitization
Sanitize all user inputs before sending them to the API to prevent injection attacks.
**Do This:**
* Use a sanitization library (e.g., DOMPurify) to escape potentially harmful characters.
**Don't Do This:**
* Trust user input without validation and sanitization.
**Why:** Input sanitization helps prevent Cross-Site Scripting (XSS) and other injection attacks.
### 6.2 Secure Storage of API Keys
Store API keys securely using environment variables and avoid committing them to source control.
**Do This:**
* Use environment variables for API keys.
* Encrypt sensitive data if necessary.
**Don't Do This:**
* Hardcode API keys directly in the code.
* Commit API keys to Git repositories.
**Why:** Improperly stored API keys can be compromised, leading to unauthorized access and data breaches.
### 6.3 CORS Configuration
Configure Cross-Origin Resource Sharing (CORS) settings properly on the server to allow requests from the application's origin.
**Do This:**
* Set appropriate CORS headers on the server.
* Restrict allowed origins to trusted domains.
**Don't Do This:**
* Allow all origins ("Access-Control-Allow-Origin: *") in production.
**Why:** CORS protects against unauthorized cross-origin requests, preventing malicious websites from accessing the application's resources.
## 7. Caching Strategies
Implement caching strategies to reduce API requests, improve performance, and lower latency.
### 7.1 Client-Side Caching
Cache API responses on the client-side using browser storage (e.g., "localStorage", "sessionStorage") or a caching library.
**Do This:**
* Use "localStorage" for persistent caching or "sessionStorage" for session-based caching.
* Implement cache invalidation strategies.
**Don't Do This:**
* Cache sensitive data on the client-side without encryption.
* Cache data indefinitely without invalidation.
**Why:** Client-side caching reduces network requests and improves the perceived performance of the application.
**Code example:**
"""typescript
import { createSignal, onMount } from 'solid-js';
const useLocalStorageCache = (key: string, initialValue: T) => {
const [cachedValue, setCachedValue] = createSignal(initialValue);
onMount(() => {
try {
const item = localStorage.getItem(key);
if (item) {
setCachedValue(JSON.parse(item));
}
} catch (error) {
console.error("Error reading from localStorage:", error);
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(cachedValue()) : value;
setCachedValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error("Error writing to localStorage:", error);
}
};
return [cachedValue, setValue] as const;
};
const fetchWithCache = async (url: string, cacheKey: string, fetchFunction: () => Promise): Promise => {
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
console.log("Serving data from cache");
return Promise.resolve(JSON.parse(cachedData) as T);
} else {
console.log("Fetching data from API");
try {
const data = await fetchFunction();
localStorage.setItem(cacheKey, JSON.stringify(data));
return data;
} catch (error) {
console.error("Error fetching data:", error);
throw error;
}
}
}
export { useLocalStorageCache, fetchWithCache };
"""
These standards are intended to evolve with the Solid.js ecosystem. Regular review and updates are essential to maintain their relevance and effectiveness. By adhering to these guidelines, Solid.js developers can create robust, maintainable, and secure 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'
# State Management Standards for Solid.js This document outlines the recommended coding standards and best practices for state management in Solid.js applications. Adhering to these standards will lead to more maintainable, performant, and robust applications. ## 1. Core Principles of State Management in Solid.js Solid.js provides fine-grained reactivity out of the box. This makes state management both powerful and potentially complex. Understanding the core principles is crucial. * **Explicit State:** Be explicit about which variables constitute your application's state. Hidden state can lead to unpredictable behavior and difficulty in debugging. * **Reactivity Boundaries:** Clearly define the boundaries of reactive updates. Avoid unnecessarily large reactive graphs to prevent needless re-renders and performance bottlenecks. * **Immutability (Conceptual):** While Solid.js doesn't enforce immutability like React, treating state updates as if they are immutable can greatly improve predictability and simplify reasoning about your application. * **Single Source of Truth:** Each piece of data should have a single, definitive source. Avoid duplicating data across multiple stores or components, as this leads to inconsistencies and synchronization issues. * **Declarative Updates:** Prefer declarative approaches to state updates. Rely on Solid's reactive primitives such as signals to propagate change. Avoid directly manipulating the DOM. ## 2. Choosing a State Management Approach Solid.js offers flexibility in state management. Choose the approach that best suits the complexity and scale of your application. ### 2.1. Signals: The Foundation Signals are the fundamental reactive primitive in Solid.js. * **Standard:** Use signals for local component state and for simple global state management. * **Why:** Signals provide fine-grained reactivity and are performant for frequent updates. * **Do This:** """javascript import { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); const increment = () => setCount(count() + 1); return ( <div> <p>Count: {count()}</p> <button onClick={increment}>Increment</button> </div> ); } """ * **Don't Do This:** Mutating the signal's value directly without using the setter function. """javascript import { createSignal } from 'solid-js'; function IncorrectCounter() { const [count, setCount] = createSignal(0); const increment = () => { // ANTI-PATTERN: Mutating the signal directly // This will not trigger reactivity! count = count() + 1; setCount(count); // Doing this after doesn't fix the problem. }; return ( <div> <p>Count: {count()}</p> <button onClick={increment}>Increment</button> </div> ); } """ * **Anti-Pattern:** Creating signals inside components unnecessarily. If a signal is only read in a component but written to elsewhere, create it outside. This avoids unnecessary re-creation on component re-renders. This is very important for frequently rendered components. """javascript // Outer scope reduces recreation and improves performance. const [externalCount, setExternalCount] = createSignal(0); function MyComponent() { return ( <div> <p>External Count: {externalCount()}</p> </div> ); } """ ### 2.2. Stores: Structured Data Management Stores provide a structured way to manage complex data in Solid.js. They offer shallow reactivity, making them ideal for managing objects and arrays. * **Standard:** Use stores for managing complex data structures and collections. Prefer "createMutable" from "solid-js/store" for mutable state, and immutable updates. For truly immutable data structures, follow patterns in section 2.4. * **Why:** Stores simplify state updates and provide built-in mechanisms for batching updates. * **Do This (Mutable Store):** """javascript import { createStore } from 'solid-js/store'; function TaskList() { const [tasks, setTasks] = createStore([ { id: 1, text: 'Learn Solid.js', completed: false }, { id: 2, text: 'Build a project', completed: false }, ]); const toggleCompleted = (id: number) => { setTasks( (task) => task.id === id, 'completed', (completed) => !completed ); }; return ( <ul> {tasks.map((task) => ( <li key={task.id}> <input type="checkbox" checked={task.completed} onChange={() => toggleCompleted(task.id)} /> <span>{task.text}</span> </li> ))} </ul> ); } """ * **Do This (Nested Updates):** Use array indices and object-property accessors within "setTasks" to perform targeted updates. Avoid unnecessarily re-creating the whole task object. """javascript import { createStore } from "solid-js/store"; function MyComponent() { const [state, setState] = createStore({ nested: { count: 0, items: ["a", "b"], }, }); const increment = () => { setState("nested", "count", (c) => c + 1); // Correct: Functional updater setState("nested", "items", 0, "new value"); // Update the first item in the nested array }; return ( <div> <p>Count: {state.nested.count}</p> <ul>{state.nested.items.map((item, i) => <li key={i}>{item}</li>)}</ul> <button onClick={increment}>Increment</button> </div> ); } """ * **Don't Do This:** Directly mutating the store object. This will not trigger reactivity. """javascript import { createStore } from "solid-js/store"; function IncorrectStore() { const [state, setState] = createStore({ count: 0 }); const increment = () => { // ANTI-PATTERN: Direct mutation state.count++; // Won't trigger reactivity! setState("count", state.count); // Does not fix the situation }; return ( <div> <p>Count: {state.count}</p> <button onClick={increment}>Increment</button> </div> ); } """ * **Anti-Pattern:** Using stores as drop-in replacements for signals when fine-grained reactivity is needed. Stores use property access to detect changes. A component that reads a complex property and only needs updates when, say, the 3rd element in an array changes would benefit from signals. ### 2.3. Context API: Implicit State Sharing The Context API provides a way to share state implicitly down the component tree. * **Standard:** Use the Context API for providing configuration values, themes, or other global settings. Do not use as a primary means of state management for frequently changing data. Prefer signals or stores in the context. * **Why:** Context provides a clean way to avoid prop drilling, but it can make data flow less explicit. * **Do This:** """javascript import { createContext, useContext, createSignal, ParentComponent } from 'solid-js'; interface ThemeContextType { theme: string; setTheme: (theme: string) => void; } const ThemeContext = createContext<ThemeContextType>({ theme: 'light', setTheme: () => {}, // Provide a default no-op function }); function ThemeProvider(props: { children: JSX.Element }) { const [theme, setTheme] = createSignal('light'); const value: ThemeContextType = { theme: theme(), setTheme: setTheme, }; return ( <ThemeContext.Provider value={value}> {props.children} </ThemeContext.Provider> ); } function ThemedComponent() { const { theme } = useContext(ThemeContext); return ( <div style={{ background: theme === 'light' ? 'white' : 'black', color: theme === 'light' ? 'black' : 'white' }}> Themed Content </div> ); } function App() { return ( <ThemeProvider> <ThemedComponent /> </ThemeProvider> ); } """ * **Don't Do This:** Overusing context for frequently changing application state. This can lead to performance issues due to unnecessary re-renders of context consumers. Instead, provide signals or stores through context. * **Anti-Pattern:** Relying solely on context for very specific state that only a few components need. Prop drilling might be more explicit and easier to maintain in such cases. ### 2.4. External Libraries: Managing Global State For larger applications, consider using dedicated state management libraries. * **Standard:** Evaluate libraries like Zustand or Effector for complex global state management involving asynchronous actions or derived state. * **Why:** These libraries provide a structured approach to managing state and side effects, improving maintainability and testability. Solid-specific solutions should be preferred. * **Effector:** Effector is a batteries include solution with stores, derived state, and asynchronous actions. It is a solid choice for complex applications where the full feature set is useful. * **Do This (Effector):** Define stores, events, and effects separately and compose them to manage state. """javascript import { createStore, createEvent, createEffect, useStore } from 'effector'; // Define a store const $count = createStore(0); // Define an event const increment = createEvent(); // Define an effect (asynchronous operation) const incrementAsync = createEffect(async () => { await new Promise(resolve => setTimeout(resolve, 500)); // Simulate async return 1; }); // Connect the event to update the store $count.on(increment, state => state + 1); $count.on(incrementAsync.doneData, (state, payload) => state + payload); // Trigger the effect on event incrementAsync.use(async () => { await new Promise(resolve => setTimeout(resolve, 500)); // Simulate async return 1; }); function Counter() { const count = useStore($count); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={incrementAsync}>Increment Async</button> </div> ); } """ * **Zustand:** Zustand is a less opinionated state management solution ideal for simpler applications. It offers a very small API with excellent performance characteristics. Zustand directly modifies the state, which can be preferable in many situations due to its simplicity. Zustand does not work well with Solid’s store. """javascript import { create } from 'zustand'; interface BearState { bears: number increase: (by: number) => void } const useBearStore = create<BearState>()((set) => ({ bears: 0, increase: (by: number) => set((state) => ({ bears: state.bears + by })), })) function MyComponent() { const bears = useBearStore(state => state.bears); const increase = useBearStore(state => state.increase); return ( <div> Bears: {bears} <button onClick={() => increase(1)}>Increase</button> </div> ) } """ * **Signals and Stores are Sufficient:** For many cases with smaller amounts of application state, signals and stores are simpler to implement and debug. Unless external libraries truly simplify the management of complex state, it is best to keep state management using the framework's primitives only for faster performance and easier debugging. ## 3. Derived State and Computation Solid.js excels at efficiently deriving state from existing signals or stores. ### 3.1. "createMemo": Caching Computed Values * **Standard:** Use "createMemo" to cache computationally expensive derived values. A memo only recalculates when its dependencies change. * **Why:** "createMemo" avoids unnecessary recalculations, improving performance. * **Do This:** """javascript import { createSignal, createMemo } from 'solid-js'; function FullName() { const [firstName, setFirstName] = createSignal('John'); const [lastName, setLastName] = createSignal('Doe'); const fullName = createMemo(() => "${firstName()} ${lastName()}"); return ( <div> <p>Full Name: {fullName()}</p> <input value={firstName()} onInput={e => setFirstName(e.currentTarget.value)} /> <input value={lastName()} onInput={e => setLastName(e.currentTarget.value)} /> </div> ); } """ * **Don't Do This:** Performing expensive calculations directly in the component render function without memoization. This can lead to performance issues when the component re-renders frequently. * **Anti-Pattern:** Overusing "createMemo" when the computation is trivial. The overhead of memoization can outweigh the benefits for simple calculations. ### 3.2. "createEffect": Reacting to State Changes * **Standard:** Use "createEffect" to perform side effects in response to state changes. * **Why:** "createEffect" automatically tracks dependencies and re-runs the effect only when those dependencies change. * **Do This:** """javascript import { createSignal, createEffect } from 'solid-js'; function Logger() { const [count, setCount] = createSignal(0); createEffect(() => { console.log('Count changed:', count()); }); const increment = () => setCount(count() + 1); return ( <div> <p>Count: {count()}</p> <button onClick={increment}>Increment</button> </div> ); } """ * **Don't Do This:** Directly manipulating the DOM outside of a "createEffect". This can lead to inconsistencies and break Solid's reactivity. * **Anti-Pattern:** Overusing "createEffect" for synchronous logic that can be handled with derived signals using "createMemo". ### 3.3. Avoiding Unnecessary Updates * **Standard:** Use "batch" to make multiple state updates in a single transaction. * **Why:** Updates will be processed at once to avoid unnecessary re-renders. * **Do This:** """javascript import { createSignal, batch } from 'solid-js'; function MultiUpdate() { const [firstName, setFirstName] = createSignal('John'); const [lastName, setLastName] = createSignal('Doe'); const updateName = (first: string, last: string) => { batch(() => { setFirstName(first); setLastName(last); }); }; return ( <div> <p>Name: {firstName()} {lastName()}</p> <button onClick={() => updateName('Jane', 'Smith')}>Update Name</button> </div> ); } """ * **Standard:** Ensure that signal values are only updated when the new value differs from the current value. Although signals can prevent updates during recomputation, they can still run when the references are the same. * **Why:** Signals that update with the same value will potentially trigger unnecessary re-renders. * **Do This:** """javascript import { createSignal } from 'solid-js'; function Example() { const [text, setText] = createSignal(''); const handleChange = (e: Event) => { const target = e.target as HTMLInputElement; if (target.value !== text()) { setText(target.value); } }; return ( <input type="text" value={text()} onChange={handleChange} /> ); } """ ## 4. Asynchronous State Updates Managing asynchronous operations and their impact on state requires careful consideration. ### 4.1. Async Functions and State * **Standard:** Use "async/await" syntax for asynchronous operations. Update state within the "async" function using signals or stores. * **Why:** "async/await" simplifies asynchronous code and makes it easier to reason about data flow. * **Do This:** """javascript import { createSignal } from 'solid-js'; function DataFetcher() { const [data, setData] = createSignal(null); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(null); const fetchData = async () => { setLoading(true); setError(null); try { const response = await fetch('https://api.example.com/data'); const result = await response.json(); setData(result); } catch (e: any) { setError(e); } finally { setLoading(false); } }; return ( <div> <button onClick={fetchData} disabled={loading()}> Fetch Data </button> {loading() && <p>Loading...</p>} {error() && <p>Error: {error().message}</p>} {data() && <pre>{JSON.stringify(data(), null, 2)}</pre>} </div> ); } """ * **Don't Do This:** Directly mutating state outside of the component's scope in an asynchronous callback. Use the signal or store's setter function. * **Anti-Pattern:** Ignoring potential errors in asynchronous operations. Always handle errors gracefully and update the state accordingly. ### 4.2. Suspense for Data Fetching * **Standard:** Use Solid.js's "<Suspense>" component to handle asynchronous data fetching and display fallback content while data is loading. * **Why:** Suspense simplifies the handling of loading states and improves the user experience. * **Do This:** *(Note: Solid Start is needed to use resource fetching)* """javascript import { createResource, Suspense } from 'solid-js'; async function fetchData(url: string) { const res = await fetch(url); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); } function DataComponent(props: { url: string }) { const [data] = createResource(() => props.url, fetchData); return ( <div> {data() ? ( <pre>{JSON.stringify(data(), null, 2)}</pre> ) : ( <p>Loading data...</p> )} </div> ); } function App() { return ( <Suspense fallback={<p>Loading...</p>}> <DataComponent url="https://api.example.com/data" /> </Suspense> ); } """ * **Don't Do This:** Manually managing loading states when Suspense can handle it automatically. ## 5. Testing State Management Thoroughly test your state management logic to ensure correctness and prevent regressions. ### 5.1. Unit Testing Signals and Stores * **Standard:** Write unit tests for individual signals and stores to verify their behavior. * **Why:** Unit tests isolate state management logic and make it easier to identify and fix bugs. * **Do This (Example with Jest):** """javascript import { createSignal } from 'solid-js'; import { createRoot } from 'solid-js/web'; describe('Counter Signal', () => { it('should increment the count', () => { createRoot(() => { const [count, setCount] = createSignal(0); setCount(1); // Increment the count expect(count()).toBe(1); }); }); }); """ ### 5.2. Integration Testing Components with State * **Standard:** Write integration tests for components that interact with state to ensure that the UI updates correctly in response to state changes. Use testing libraries like "@testing-library/solid" or Cypress. * **Why:** Integration tests verify that the state management logic is correctly connected to the UI. ## 6. Accessibility Considerations * **Standard:** Ensure that state changes are reflected in an accessible manner for users with disabilities. Use ARIA attributes to provide context and status updates to assistive technologies. * **Why:** Accessibility is a core principle of inclusive design. * **Do This:** Use ARIA live regions to announce state updates to screen readers. """javascript import { createSignal } from 'solid-js'; function StatusMessage() { const [message, setMessage] = createSignal(''); const updateMessage = (newMessage: string) => { setMessage(newMessage); }; return ( <div aria-live="polite" aria-atomic="true"> {message()} <button onClick={() => updateMessage('Data loaded successfully')}>Load Data</button> </div> ); } """ ## 7. Performance Optimization * **Standard:** Limit re-renders by only updating the parts of the UI that need to change. * **Why:** Updating only what is necessary improves performance and reduces wasted resources. ### 7.1. Granular Updates Solid's fine-grained reactivity system makes it easier to only update the parts of the UI that need to change. Avoid unnecessary re-renders by ensuring that components only subscribe to the specific state they need. ### 7.2. Memoization Use "createMemo" to cache the results of expensive computations. This can prevent the need to re-compute the same value multiple times. Always keep in mind the overhead of "createMemo". ### 7.3. Virtualized Lists Use a virtualized list component for displaying large lists of data. A virtualized list only renders the items that are currently visible on the screen. ## 8. Security Considerations * **Standard:** Sanitize user input before storing it in state to prevent XSS attacks. * **Why:** Security is paramount. Failing to sanitize user input can lead to vulnerabilities that compromise the application. This comprehensive guide provides a foundation for building robust and maintainable Solid.js applications with effective state management strategies. By adhering to these standards, developers can ensure code quality, performance, and security.
# Tooling and Ecosystem Standards for Solid.js This document outlines the recommended tooling and ecosystem standards for Solid.js development. Adhering to these standards will improve code quality, maintainability, performance, and collaboration within your team. ## 1. Recommended Libraries and Tools Here's a curated list of recommended libraries and tools that complement Solid.js, along with guidelines for their proper usage: ### 1.1. State Management * **zustand:** Simple, fast, and unopinionated bearbones state-management solution using stores. * **Do This:** Use "zustand" for simple to medium complexity global state management needs. * **Don't Do This:** Overuse "zustand" for component-local state; use Solid's built-in signals. Rely on "zustand" for highly complex state scenarios with many dependencies. * **Why:** "zustand" is lightweight and easy to integrate with Solid's reactivity. For highly complex scenarios, consider more robust solutions like effector with solid-effector. """jsx import { create } from 'zustand'; import { createStore, createEffect } from 'solid-js'; const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); function Counter() { const { count, increment, decrement } = useStore(); return ( <div> Count: {count} <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); } """ * **effector (with solid-effector):** A powerful and type-safe state management library with reactive primitives and unidirectional data flow. * **Do This:** Utilize effector for complex application state, side-effect management, and handling asynchronous logic. * **Don't Do This:** Reinvent the wheel for common state management patterns. Ignore the type safety and traceability benefits effector provides. * **Why:** Effector provides excellent tools for side-effect management and scales well with application complexity. """jsx import { createStore, createEvent } from 'effector'; import { useStore } from 'solid-effector'; import { render } from 'solid-js/web'; const increment = createEvent(); const decrement = createEvent(); const $count = createStore(0) .on(increment, state => state + 1) .on(decrement, state => state - 1); function Counter() { const count = useStore($count); return ( <> Count: {count()} <button onClick={() => increment()}>Increment</button> <button onClick={() => decrement()}>Decrement</button> </> ); } """ ### 1.2. Routing * **solid-app-router:** The officially recommended router for Solid.js applications. * **Do This:** Use "solid-app-router" for all routing needs in single-page applications. Utilize its "<Route>" and "<Link>" components. * **Don't Do This:** Attempt to create your own routing solution; use the official router. Bind directly to "window.location" (use the router's APIs instead). * **Why:** "solid-app-router" is specifically designed for Solid.js, offering excellent performance and integration with Solid's reactivity system. """jsx import { Route, Link, Routes } from "solid-app-router"; import { Component } from 'solid-js'; const Home:Component = () => <h1>Home</h1>; const About:Component = () => <h1>About</h1>; function App() { return ( <div> <nav> <Link href="/">Home</Link> | <Link href="/about">About</Link> </nav> <Routes> <Route path="/" component={Home} /> <Route path="/about" component={About} /> </Routes> </div> ); } import { render } from 'solid-js/web'; render(() => <App />, document.getElementById('app')); """ ### 1.3. Form Handling * **@felte/solid:** A form library that does not use uncontrolled components. * **Do This:** Integrate with "@felte/solid" to handle form states and validations. * **Don't Do This:** Manually manage form state (onChange handlers). Use "defaultValue" instead of "value" as it leverages Solid's reactivity. * **Why:** Felte simplifies form handling by abstracting the complexities of state management and reactivity. """jsx import { createForm } from '@felte/solid'; function MyForm() { const { form, data, errors } = createForm({ onSubmit: (values) => { alert(JSON.stringify(values)); } }); return ( <form use:form> <label> First Name: <input type="text" name="firstName" /> </label> <label> Last Name: <input type="text" name="lastName" /> </label> <button type="submit">Submit</button> </form> ); } """ ### 1.4. UI Libraries * **Headless UI + Solid.js:** Combine the accessibility and flexibility of Headless UI with Solid's performance. * **Do This:** Use controlled components with signals. Implement custom styling for your components. * **Don't Do This:** Directly manipulate the DOM of Headless UI components. * **Why:** Headless UI provides the accessible logic and ARIA attributes, while Solid.js handles the reactivity and performance. """jsx import { createSignal } from 'solid-js'; import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/solid'; function MyDropdown() { const [isOpen, setIsOpen] = createSignal(false); return ( <Menu> <MenuButton onClick={() => setIsOpen(!isOpen())}> Options </MenuButton> <MenuItems> <MenuItem> <button>Edit</button> </MenuItem> <MenuItem> <button>Delete</button> </MenuItem> </MenuItems> </Menu> ); } """ * **Hope UI + Solid.js":** Use a component UI library that is built with Solid.js. * **Do This:** Integrate component libraries such as Hope UI to build faster. * **Don't Do This:** Create the wheel using a framework component library that is not built specifically with and for Solid.js. ### 1.5. Utility Libraries * **clsx:** A tiny (239B) utility for constructing "className" strings conditionally. * **Do This:** Use "clsx" to manage CSS classes based on component state. * **Don't Do This:** Manually construct class names using string concatenation (prone to errors). * **Why:** "clsx" simplifies conditional class name management, improving readability and reducing errors. """jsx import clsx from 'clsx'; import { createSignal } from 'solid-js'; function MyComponent() { const [isActive, setIsActive] = createSignal(false); const className = clsx('base-class', { 'active-class': isActive(), 'disabled-class': false, }); return ( <div className={className}> My Component <button onClick={() => setIsActive(!isActive())}>Toggle</button> </div> ); } """ ### 1.6. Testing * **Vitest:** A Vite-native test framework with lightning-fast speed. * **Do This:** Use Vitest for unit and integration testing. Configure your tests to run in a browser environment. * **Don't Do This:** Use outdated or slow test runners that don't integrate well with Vite. * **Why:** Vitest provides excellent performance and a smooth developer experience when testing Solid.js components. * **Example "vitest.config.ts":** """typescript import { defineConfig } from 'vitest/config'; import solid from 'vite-plugin-solid'; export default defineConfig({ plugins: [solid()], test: { environment: 'jsdom', // Or 'happy-dom' transformMode: { web: [/.[jt]sx?$/], }, }, }); """ ### 1.7. Formatting and Linting * **Prettier:** An opinionated code formatter. * **ESLint:** A tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. * **typescript-eslint:** Allows ESLint to lint TypeScript code. * **Do This:** Configure ESLint and Prettier to work together. Use a shareable ESLint configuration (e.g., "eslint-config-prettier", "@typescript-eslint/recommended"). * **Don't Do This:** Skip code formatting and linting. Ignore warnings and errors from your linter. * **Why:** Consistent formatting and linting improve code readability and help catch potential errors early. * **Example ".eslintrc.cjs":** """javascript module.exports = { root: true, env: { browser: true, es2020: true, node: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:solid/recommended', 'prettier', ], plugins: ['@typescript-eslint', 'solid', 'prettier'], rules: { 'prettier/prettier': ['error'], }, }; """ ### 1.8. Build Tooling * **Vite:** Extremely fast and lightweight build tool. * **Do This:** Use Vite for development and production builds. Take advantage of its hot module replacement (HMR) for faster development feedback. * **Don't Do This:** Use older build tools that are significantly slower. * **Why:** Vite provides a superior development experience due to its speed and ease of configuration. The Solid.js template is based on Vite. ### 1.9. Solid CLI * **degit:** Scaffolding tool to create new Solid.js projects based on templates. * **Do This:** Use "degit" to scaffold new solid projects. * **Don't Do This:** Manually create directory structure and install dependencies. * **Why:** Saves time and ensures consistency across projects. """bash npx degit solidjs/templates/js my-solid-project cd my-solid-project npm install npm run dev """ ## 2. Tool Configuration and Best Practices ### 2.1. Vite Configuration * **Standard:** Use the "vite-plugin-solid" plugin for seamless integration with Solid.js. * **Do This:** Include "vite-plugin-solid" in your "vite.config.ts" file. Configure any necessary options for JSX transformation. * **Don't Do This:** Forget to install or configure the plugin. * **Why:** "vite-plugin-solid" handles JSX transformation and provides other optimizations for Solid.js. * **Example "vite.config.ts":** """typescript import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; export default defineConfig({ plugins: [solid()], }); """ ### 2.2. TypeScript Configuration * **Standard:** Use strict TypeScript settings for enhanced type safety. * **Do This:** Enable "strict: true" in your "tsconfig.json" file along with ""jsx": "preserve"""jsxImportSource": "solid-js"". * **Don't Do This:** Disable strict mode; it helps catch potential type errors. * **Why:** Strict mode provides better type checking and helps prevent runtime errors. * **Example "tsconfig.json":** """json { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "jsx": "preserve", "jsxImportSource": "solid-js", "types": ["vite/client"], "noEmit": true, "isolatedModules": true, "esModuleInterop": true, "declaration": true, "declarationMap": true, "sourceMap": true, "strict": true, "skipLibCheck": true, }, "include": ["src"], } """ ### 2.3. Editor Configuration * **Standard:** Configure your editor with extensions for Solid.js syntax highlighting, code completion, and linting. * **Do This:** Install the official Solid.js extension for VS Code or similar editor. * **Don't Do This:** Use a plain text editor without language support; it hinders productivity and can lead to errors. * **Why:** Editor extensions enhance the developer experience by providing code completion, syntax highlighting, linting, and other useful features. ### 2.4. Environment Variables * **Standard:** Manage environment-specific configuration with ".env" files and Vite's "import.meta.env". * **Do This:** Prefix environment variables with "VITE_" to expose them to the client. * **Don't Do This:** Hardcode sensitive information directly in your source code. * **Why:** Centralizes configuration and prevents secrets from being committed to source control. ### 2.5. Server Side Rendering * **Standard:** Use "solid-start" meta-framework to build scalable and performant web apps. * **Do This:** Integrate "solid-start" with your project to leverage SSR with Solid. * **Don't Do This:** Roll out a custom solution for basic functionalities offered by the framework. * **Why:** "solid-start" integrates best with Solid.js. ## 3. Code Organization and Modularity ### 3.1. Component Structure * **Standard:** Organize components into logical directories based on feature or domain. * **Do This:** Create separate directories for each component, including its related styles, tests, and documentation. * **Don't Do This:** Dump all components into a single directory; it becomes difficult to manage as the application grows. * **Why:** Organizes the structure of the code. Easier to reason about. ### 3.2. Module Bundling * **Standard:** Leverage ES modules for code splitting and lazy loading. * **Do This:** Use dynamic imports ("import()") to load modules on demand. Configure Vite to optimize your bundle for production. * **Don't Do This:** Load all modules upfront, as it can increase initial load time. * **Why:** Improves the initial load time. ### 3.3. Dependency Injection * **Standard:** Utilize Solid's Context API or custom signals for dependency injection. * **Do This:** Create a context provider to share dependencies across components. * **Don't Do This:** Pass dependencies through multiple layers of components (prop drilling). * **Why:** Simplifies the dependency management. ## 4. Performance Optimization ### 4.1. Fine-Grained Updates * **Standard:** Leverage Solid's fine-grained reactivity system for efficient UI updates. * **Do This:** Use signals, memos, and effects to manage state and side effects. Avoid unnecessary component re-renders. * **Don't Do This:** Force updates via global state without considering fine-grained reactivity. * **Why:** Optimized efficient, reactive state management. ### 4.2. Lazy Loading and Code Splitting * **Standard:** Implement lazy loading for components and modules that are not immediately needed. * **Do This:** Use the "lazy" and "Suspense" components. * **Don't Do This:** Load all components at once. Increase size of initial load. * **Why:** Lazy components improve the user experience by providing faster initial load times. ### 4.3. Virtualization * **Standard:** Use virtualization techniques for rendering large lists of data. * **Do This:** Employ libraries like "solid-virtual" to render only the visible items. * **Don't Do This:** Render entire lists at once if working with datasets of significant size, or with large render complexity in the list items. """jsx import { createSignal, onMount } from 'solid-js'; import { Virtualizer } from 'solid-virtual'; interface Item { id: number; name: string; } function MyList() { const [items, setItems] = createSignal<Item[]>([]); onMount(() => { // Simulate fetching data setTimeout(() => { const fakeData = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: "Item ${i}", })); setItems(fakeData); }, 500); }); return ( <Virtualizer<Item> items={items()} size={50} overscan={10} > {(item) => ( <div style={{ height: '50px' }}> {item.name} </div> )} </Virtualizer> ); } export default MyList; """ ### 4.4. Memoization * **Standard:** Use memoization to avoid re-computing values that have not changed. * **Do This:** Leverage Solid's "createMemo" function to cache computed values based on their dependencies. * **Don't Do This:** Create memos for values that are not computationally intensive. * **Why:** memoization reduces the amount of work the CPU has to do. ## 5. Security Best Practices ### 5.1. Input Validation * **Standard:** Validate all user inputs to prevent malicious data from entering your application. * **Do This:** Use a validation library to enforce input constraints; sanitize data using libraries like DOMPurify. * **Don't Do This:** Trust user input without validation; it can lead to security vulnerabilities like XSS and SQL injection. * **Why:** Prevents against hackers and other malicious agents. ### 5.2. Cross-Site Scripting (XSS) Prevention * **Standard:** Sanitize user-generated content to prevent XSS attacks which can be injected through user controlled inputs. * **Do This:** Use template literals with escaping or libraries like DOMPurify to sanitize HTML content. * **Don't Do This:** Directly render user-provided HTML without sanitization. * **Why:** Prevents malicious code injection. ### 5.3. Secure API Communication * **Standard:** Implement secure API communication using HTTPS and proper authentication/authorization mechanisms. * **Do This:** Always use HTTPS for API requests. Use JSON Web Tokens (JWT) for authentication and authorization. * **Don't Do This:** Transmit sensitive data over HTTP; it is vulnerable to interception. Store private API keys directly in the code. * **Why:** Secures data transfers. ### 5.4. Dependency Management * **Standard:** Regularly audit and update your dependencies to patch known security vulnerabilities. * **Do This:** Use "npm audit" or "yarn audit" to identify vulnerabilities in your dependencies. Keep your dependencies up-to-date. * **Don't Do This:** Ignore security warnings from your package manager; they can indicate serious vulnerabilities. * **Why:** Reduce vulnerability exposure. ## 6. Observability and Debugging ### 6.1. Logging * **Standard:** Implement comprehensive logging to track application behavior and diagnose issues. * **Do This:** Use console.log, console.warn, and console.error for debugging purposes. * **Don't Do This:** Neglect to log key events and errors; it makes debugging difficult. Put sensitive information in logs. ### 6.2. Error Handling * **Standard:** Implement robust error handling to prevent application crashes. * **Do This:** Use centralized error handling with "ErrorBoundary" * **Don't Do This:** Allow errors to propagate unhandled; it can lead to a poor user experience. * **Why:** Provides a robust and reliable user experience. ### 6.3. Browser DevTools * **Standard:** Use the browser's developer tools for debugging and performance analysis. * **Do This:** Use the browser's JavaScript debugger to step through code and inspect variables. Use the performance profiler to identify bottlenecks. Use the network panel to analyze network requests. * **Don't Do This:** Fail to leverage the dev tools; they are essential for debugging and optimization. ## 7. Collaboration and Documentation ### 7.1. Code Reviews * **Standard:** Conduct thorough code reviews to ensure code quality and prevent errors. * **Do This:** Review code changes before they are merged into the main branch. Provide constructive feedback. * **Don't Do This:** Skip code reviews; they are essential for catching potential issues. ### 7.2. Documentation * **Standard:** Maintain comprehensive documentation for your components, modules, and APIs. * **Do This:** Use JSDoc or TypeScriptDoc to document your code. * **Don't Do This:** Neglect documentation. * **Why:** Easily read and understood codebase. ### 7.3. Version Control * **Standard:** Use Git for version control. * **Do This:** Use a branching strategy (e.g. Gitflow). Make small, focused commits with descriptive messages. Use pull requests for code review. * **Don't Do This:** Commit directly to the main branch. * **Why:** Easily track and trace changes. By adhering to these tooling and ecosystem standards, you can ensure the maintainability, performance, and security of your Solid.js applications.
# Performance Optimization Standards for Solid.js This document outlines coding standards for optimizing performance in Solid.js applications. It provides guidelines, best practices, and code examples to help developers build fast, responsive, and efficient applications utilizing the benefits of Solid.js reactivity at its best. ## 1. Leveraging Solid's Reactivity ### 1.1 Fine-Grained Reactivity **Standard:** Utilize Solid's fine-grained reactivity system to minimize unnecessary re-renders and computations. **Why:** Solid.js uses signals, derived signals (memos), and effects to automatically track dependencies and update only the components that need to be updated. This approach drastically reduces wasted computation time compared to virtual DOM frameworks that always re-render the template starting from a large component and diff the resulting DOM. **Do This:** * Favor signals for managing state that changes over time. * Use memos to derive computed values from signals efficiently. * Use effects to perform side effects that react to signal changes. * Structure components to isolate reactive updates to the smallest possible DOM nodes. **Don't Do This:** * Rely on global state management libraries that bypass Solid's reactivity. * Over-use effects for general computations better suited for memos. * Manually manipulate the DOM – let Solid.js handle updates. **Code Example:** """jsx import { createSignal, createMemo, createEffect } from 'solid-js'; import { render } from 'solid-js/web'; function MyComponent() { const [count, setCount] = createSignal(0); const doubledCount = createMemo(() => count() * 2); createEffect(() => { console.log('Count changed:', count()); }); return ( <div> <p>Count: {count()}</p> <p>Doubled Count: {doubledCount()}</p> <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); } render(() => <MyComponent />, document.getElementById('root')); """ In this example, only the text nodes displaying "count" and "doubledCount" will update when "count" changes. The effect only runs when "count" updates ensuring only necessary side effects occur. ### 1.2 Avoid Unnecessary Global State Updates **Standard:** Only trigger signal updates when new values differ from the previous ones, especially in global stores. **Why:** Updating signals unnecessarily can lead to useless re-renders and effect triggers, even if the resulting DOM remains the same. Solid will prevent rerunning effects when values haven't changed, but reducing signal updates saves on the equality check. **Do This:** * Implement a shallow comparison before updating signals with potentially unchanged values, particularly in complex objects. * Use the "setState" pattern introduced in Solid.js to merge updates, avoiding replacing the entire state object in global stores. * Structure global state to minimize dependencies between different parts of the application. **Don't Do This:** * Blindly update global signals without checking for changes. * Rely on deeply nested global state structures that propagate updates across unrelated components. **Code Example:** """jsx import { createSignal } from 'solid-js'; const [user, setUser] = createSignal({ id: 1, name: 'Initial User', address: { city: 'Some City' } }); function updateUserCity(newCity) { setUser(prevUser => { if (prevUser.address.city === newCity) { return prevUser; // Avoid unnecessary signal update } return { ...prevUser, address: { ...prevUser.address, city: newCity } }; }); } //Later, on a button handler <button onClick={() => updateUserCity('New City')}>Update City</button> """ This example updates only the "city" property of the "address" object within the "user" signal *if* the city is different. It carefully utilizes the "setUser" updater function to avoid unnecessary re-renders. ### 1.3 Memoization vs. Derived Signals **Standard:** Understand the distinction between memoization ("createMemo") and derived signals, and choose the appropriate approach. **Why:** "createMemo" caches the result of a computation and only re-executes it when its dependencies change. This is perfect for derived values that are consumed by the UI. A derived signal is returned directly from the source signal. Using the right tool for the right job contributes to performance. **Do This:** * Use "createMemo" for UI-bound, costly computations based on signals. Especially computations that trigger DOM updates. * Favor derived signals for internal computations or temporary values in functions. * Review and optimize existing memos if your application is underperforming. **Don't Do This:** * Overuse "createMemo" for cheap computations (direct signal access is usually faster). * Neglect to update memos when their underlying dependencies change (causing stale data). * Chain several memos together, creating long dependency chains. **Code Example:** """jsx import { createSignal, createMemo } from 'solid-js'; function ComplexComponent() { const [input, setInput] = createSignal(''); const processedInput = createMemo(() => { console.log("Processing input (expensive op)"); let temp = input().toUpperCase(); // Simulate expensive string processing operation for (let i = 0; i < 1000; i++) { temp += i.toString(); } return temp; }); return ( <div> <input type="text" value={input()} onInput={(e) => setInput(e.currentTarget.value)} /> <p>Processed Input: {processedInput()}</p> </div> ); } export default ComplexComponent; """ In this example, the "processedInput" memo ensures that the expensive string processing is only executed when the "input" signal actually changes, preventing unnecessary re-computations. ## 2. Component Design Patterns for Performance ### 2.1 List Virtualization **Standard:** Implement list virtualization for rendering large lists of data. **Why:** Rendering a very long list can be slow because of the high number of elements that need to be created and rendered in the beginning. List virtualization only renders the part of the list that's currently visible and renders more elements as the user scrolls. **Do This:** * Use libraries like "solid-virtual" for efficient list virtualization. * Calculate the height of each list item accurately for accurate virtualization calculations. * Implement placeholders or loading indicators for items that are not yet rendered. **Don't Do This:** * Render thousands of items at once without virtualization. * Use imprecise or variable item heights, causing rendering glitches. **Code Example:** """jsx import { createSignal } from 'solid-js'; import { render } from 'solid-js/web'; import { VirtualList } from 'solid-virtual'; function VirtualizedList() { const [items, setItems] = createSignal(Array.from({ length: 1000 }, (_, i) => "Item ${i}")); return ( <VirtualList items={items()} itemSize={50} // Estimated height of each item > {(item) => ( <div style={{ height: '50px', borderBottom: '1px solid #ccc' }}>{item}</div> )} </VirtualList> ); } render(() => <VirtualizedList />, document.getElementById('root')); """ This example utilizes the "solid-virtual" library to efficiently render a long list of items with a fixed height, only rendering visible elements. ### 2.2 Code Splitting and Lazy Loading **Standard:** Implement code splitting to reduce initial load time and lazily load components as needed. **Why:** Splitting your code into smaller chunks allows the browser to download and parse only the code required for the initial view, improving the time to interactive. Lazy loading defers the loading of non-critical components until they are actually needed, further reducing the initial payload. **Do This:** * Utilize Solid's "lazy" function for dynamic imports of components. * Group related components into logical bundles for splitting. * Use suspense to display a loading indicator while lazy components are loading. **Don't Do This:** * Put all your code into a single, massive bundle. * Lazily load components that are essential for the initial view. * Neglect to provide a suspense fallback, causing a blank screen while loading. **Code Example:** """jsx import { lazy, Suspense } from 'solid-js'; import { Route, Routes } from '@solidjs/router'; const Home = lazy(() => import('./Home')); const About = lazy(() => import('./About')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> ); } export default App; """ This example shows how to use "lazy" along with "<Suspense>" from Solid.js to lazy load components when requested inside the "Routes" component. Using a suspense fallback will handle loading states of each lazy-loaded component. ### 2.3 Pre-rendering & Server-Side Rendering (SSR) **Standard:** Consider pre-rendering or SSR for improved initial performance and SEO. **Why:** Pre-rendering generates static HTML at build time, which can be served immediately to the user, reducing the time to first contentful paint (FCP) and improving SEO. SSR renders the application on the server for the first request, providing similar benefits to pre-rendering but allowing for more dynamic content. **Do This:** * Use a framework like Solid Start for SSR and static site generation. * Analyze the needs of your application to determine if pre-rendering or SSR is the most suitable approach. * Implement proper caching strategies for SSR to minimize server load. **Don't Do This:** * Use SSR without proper caching, causing excessive server load. * Use SSR for purely static content that would be better served by pre-rendering. * Overcomplicate SSR with unnecessary features or dependencies. **Code Example (Solid Start):** Solid Start automatically handles routing, SSR and static site generation. """jsx // Filename: src/routes/index.tsx import { createSignal } from "solid-js"; export default function Index() { const [count, setCount] = createSignal(0); return ( <section> <h1>Hello world!</h1> <p> <button onClick={() => setCount(count() + 1)}> {count()} </button> </p> </section> ); } """ ## 3. Data Fetching Optimization ### 3.1 Caching Data Requests **Standard:** Implement client-side caching for data requests. **Why:** Repeatedly fetching the same data from an API is inefficient. Caching data on the client can significantly reduce network requests and improve perceived performance, specially for data rarely modified. **Do This:** * Use a simple in-memory cache for short-lived data. * Use "localStorage" or "sessionStorage" for persistent caching (use with caution). * Use a dedicated caching library like "swr" or implement your own caching mechanism using signals and memos. * Employ cache invalidation strategies to update cached data when necessary. **Don't Do This:** * Cache sensitive data in "localStorage" without proper encryption. * Cache data indefinitely without invalidation, causing stale data to be displayed. * Rely solely on browser caching without implementing a client-side caching layer. **Code Example:** """jsx import { createSignal, createEffect } from 'solid-js'; function useCachedData(url, fetcher) { const [data, setData] = createSignal(null); const [isLoading, setIsLoading] = createSignal(false); const [error, setError] = createSignal(null); const cache = new Map(); // Simple in-memory cache createEffect(() => { async function fetchData() { setIsLoading(true); try { if (cache.has(url)) { console.log('Data from cache'); setData(cache.get(url)); } else { const result = await fetcher(url); cache.set(url, result); setData(result); console.log('Data from API'); } } catch (e) { setError(e); } finally { setIsLoading(false); } } fetchData(); }); return { data, isLoading, error }; } async function myFetcher(url) { const response = await fetch(url); //Example return response.json(); } // Usage function MyComponent() { const { data, isLoading, error } = useCachedData('https://api.example.com/data', myFetcher); if (isLoading()) return <p>Loading...</p>; if (error()) return <p>Error: {error().message}</p>; if (!data()) return <p>No data available.</p>; return ( <div> {/* Render your data here */} <pre>{JSON.stringify(data(), null, 2)}</pre> </div> ); } """ This example shows a basic "useCachedData" hook that fetches data from an API and caches it in memory. If the data is already cached, it returns the cached value instead of making a new API request. ### 3.2 Request Prioritization **Standard:** Prioritize data requests to improve perceived performance. **Why:** Fetching critical data first allows the user to see meaningful content sooner. **Do This:** * Use techniques like "Promise.all" to fetch non-critical data in parallel. * Defer fetching of low-priority data until after the initial view is rendered. * Use "fetchPriority" attribute to instruct the browser to prioritize loading of specific network resources. **Don't Do This:** * Block the rendering of the initial view while waiting for all data to load. * Unnecessarily chain requests, creating a waterfall effect. * Ignore the priority of different data requests. **Code Example:** """jsx import { createSignal, createEffect } from 'solid-js'; function prioritizeFetch() { const [primaryData, setPrimaryData] = createSignal(null); const [secondaryData, setSecondaryData] = createSignal(null); createEffect(() => { async function fetchPrimary() { const response = await fetch('/api/primary'); setPrimaryData(await response.json()); } async function fetchSecondary() { const response = await fetch('/api/secondary'); setSecondaryData(await response.json()); } // Start fetching primary data immediately fetchPrimary(); // Fetch secondary data after the component has rendered (using setTimeout 0) setTimeout(fetchSecondary, 0); }); return { primaryData, secondaryData }; } function MyComponent() { const { primaryData, secondaryData } = prioritizeFetch(); return ( <div> <h1>{primaryData()?.title || 'Loading...'}</h1> <p>{secondaryData()?.description || 'Loading more content...'}</p> </div> ); } """ In this example, the primary data is fetched immediately, while the secondary data is fetched after a small delay using "setTimeout(...,0)". This will render primary data sooner for better a user experience. ### 3.3 Batching API Requests **Standard:** Combine multiple API requests into a single request when possible. **Why:** Reducing the number of network requests can significantly improve performance, especially in scenarios where multiple small pieces of data are needed. **Do This:** * Use GraphQL or similar technologies to fetch multiple related data points in a single query. * Implement batch endpoints on your backend to handle multiple requests in a single API call. * Use libraries like "axios" to group fetch requests. **Don't Do This:** * Make an excessive number of small API requests when a single batched request would suffice. * Over-complicate batching logic with unnecessary complexity. * Send unnecessarily large payloads in batched requests. ## 4. DOM Manipulation ### 4.1 Efficient DOM Updates in Effects **Standard:** Ensure effect blocks perform minimal DOM manipulations. **Why:** While Solid is efficient, DOM manipulation is still relatively slow. Minimize the scope of necessary operations to improve the overall responsiveness. **Do This:** * Update only the specific DOM nodes that need to be changed within an effect. * Avoid unnecessary DOM reads, as they can trigger layout thrashing. * Throttle or debounce effects that trigger frequently to avoid overloading the browser. Use utility libraries like "lodash" that support throttling and debouncing to avoid re-implementing and testing those solutions. **Don't Do This:** * Rely on effects to manipulate large portions of the DOM unnecessarily. * Perform synchronous DOM reads and writes within the same effect, causing layout thrashing. * Trigger effects at excessively high frequencies. **Code Example:** """jsx import { createSignal, createEffect } from 'solid-js'; function EfficientDOMUpdates() { const [text, setText] = createSignal('Initial Text'); createEffect(() => { const element = document.getElementById('my-element'); // Get the element ONCE, outside the update cycle if (element) { element.textContent = text(); // Efficiently updates the text content } }); return ( <div> <p id="my-element">{text()}</p> <input type="text" value={text()} onInput={(e) => setText(e.currentTarget.value)} /> </div> ); } """ This example retrieves the DOM element only once, then updates its text content directly within the effect, avoiding unnecessary re-renders and DOM manipulations. The element retrieval can be done only once since the element exists on initial render. ### 4.2 Avoiding DOM thrashing **Standard:** Minimize synchronous DOM reads immediately followed by DOM writes to avoid layout thrashing. **Why:** When the browser is forced to recalculate styles and layout due to interleaved reads and writes, performance degrades significantly. **Do this:** * Batch your DOM reads and writes. First performs all the reads needed to base your logic on and then proceeds with all writes. * Use "requestAnimationFrame" API to read and write to DOM. **Don't Do This:** * Mix synchronous DOM reads and writes in your logic. **Code Example:** """jsx import { createSignal, createEffect } from 'solid-js'; function AvoidDOMThrashing() { const [width, setWidth] = createSignal('200'); createEffect(() => { requestAnimationFrame(() => { // All reads here... const element = document.getElementById('thrashing-element'); // Get the element ONCE, outside the update cycle if (element) { const elementWidth = element.offsetWidth; console.log("Element width: ${elementWidth}"); // All writes here... element.style.width = "${width()}px"; // Synchronously sets the element width } }); }); return ( <div> <p id="thrashing-element" style={{width: "200px", height: "50px", backgroundColor: "red"}}></p> <input type="number" value={width()} onInput={(e) => setWidth(e.currentTarget.value)} /> </div> ); } """ ### 4.3 Template Literal Optimizations **Standard:** Use template literals or tagged template literals efficiently to construct dynamic strings or HTML. **Why:** Building strings for DOM manipulation can be slow if performed incorrectly. Template literals are generally more efficient than string concatenation, but tagged template literals offer more advanced optimization possibilities. **Do This:** * Use template literals for simple string interpolation. * Use tagged template literals with memoization to cache the generated DOM structure for repeated usage. * Minimize string operations within template literals. **Don't Do This:** * Rely on expensive string concatenation operations for dynamic content. * Overuse tagged template literals for simple string interpolations. **Code Example:** """jsx import { createSignal, createMemo } from 'solid-js'; function TemplateOptimizations() { const [name, setName] = createSignal('World'); // Basic template literal const greeting = "Hello, ${name()}!"; return ( <div> <p>{greeting}</p> <input type="text" value={name()} onInput={(e) => setName(e.currentTarget.value)} /> </div> ); } """ ## 5. Memory Management ### 5.1 Properly Disposing of Resources **Standard:** Dispose of resources (e.g., timers, event listeners, subscriptions) when they are no longer needed. **Why:** Failure to dispose of resources can lead to memory leaks, negatively impacting performance and potentially crashing the application in the long run. **Do This:** * Use the "onCleanup" hook to dispose of resources when a component unmounts. * Clear timers using "clearInterval" or "clearTimeout". * Unsubscribe from event listeners using "removeEventListener". * Dispose of subscriptions to external data sources. **Don't Do This:** * Ignore the cleanup of resources, relying on garbage collection to handle everything. * Create global resources without proper cleanup mechanisms. * Leak resources across component unmounts. **Code Example:** """jsx import { createSignal, createEffect, onCleanup } from 'solid-js'; function ResourceCleanup() { const [count, setCount] = createSignal(0); createEffect(() => { const intervalId = setInterval(() => { setCount(prev => prev + 1); }, 1000); onCleanup(() => { clearInterval(intervalId); console.log('Interval cleared.'); }); }); return ( <div> <p>Count: {count()}</p> </div> ); } """ In this example, the "clearInterval" call ensures that the interval is cleared when the component unmounts, preventing a memory leak. ### 5.2 Managing Large Data Structures **Standard:** Efficiently manage large data structures to minimize memory consumption. **Why:** Loading and processing large datasets can quickly consume memory, leading to performance issues and browser crashes. **Do This:** * Use techniques like pagination or infinite scrolling to load data in chunks. * Utilize data structures that are optimized for memory efficiency (e.g., Typed Arrays). * Consider using a database or specialized data storage solution for extremely large datasets. **Don't Do This:** * Load entire datasets into memory at once. * Store large data structures in global variables without proper management. * Use inefficient data structures for large datasets. ## 6. Tooling and Auditing ### 6.1 Profiling with Solid Devtools **Standard:** Utilize the Solid Devtools to profile your application and identify performance bottlenecks. **Why:** The Solid Devtools provide valuable insights into the reactivity graph, component rendering, and overall performance of your application, helping you identify areas for improvement. **Do This:** * Install the Solid Devtools browser extension. * Use the profiler to record and analyze component rendering times. * Examine the reactivity graph to understand the data flow. * Identify and optimize expensive computations or unnecessary re-renders. **Don't Do This:** * Ignore the insights provided by the Solid Devtools. * Rely solely on guesswork when diagnosing performance problems. * Neglect to profile your application before deploying to production. ### 6.2 Lighthouse Audits **Standard:** Regularly run Lighthouse audits to assess the performance of your application. **Why:** Lighthouse provides a comprehensive set of performance metrics and recommendations for improvement, covering areas such as loading speed, accessibility, and SEO. **Do This:** * Run Lighthouse audits in Chrome Devtools or using the command-line tool. * Address the recommendations provided by Lighthouse to improve overall performance. * Pay attention to key metrics such as First Contentful Paint (FCP), Largest Contentful Paint (LCP), and Time to Interactive (TTI). **Don't Do This:** * Ignore the warnings and errors reported by Lighthouse. * Focus solely on achieving a perfect Lighthouse score without considering the actual user experience. * Neglect to run Lighthouse audits after making significant changes to your application. ### 6.3 Performance Budgeting **Standard:** Define and enforce a performance budget for your application. **Why:** A performance budget sets clear targets for key performance metrics, helping you maintain a consistently fast and responsive application throughout the development lifecycle. **Do This:** * Define performance budgets for metrics such as page load time, bundle size, and time to interactive. * Use tools like "Bundle Analyzer" and "Lighthouse" to track your progress against the budget. * Integrate performance checks into your CI/CD pipeline to prevent regressions. **Don't Do This:** * Set unrealistic or overly ambitious performance budgets. * Ignore performance budgets once they have been defined. * Fail to enforce performance budgets throughout the development process. By adhering to these standards, developers can build high-performance Solid.js applications that provide a superior user experience while minimizing resource consumption.
# Deployment and DevOps Standards for Solid.js This document outlines the standards and best practices for deploying and managing Solid.js applications. It covers build processes, CI/CD pipelines, production environment considerations, and monitoring, emphasizing Solid.js-specific aspects and modern DevOps approaches. ## 1. Build Processes and Optimization ### 1.1. Use a Modern Build Toolchain **Do This:** Utilize a modern JavaScript build toolchain like Vite, Rollup or Parcel for bundling and optimizing your Solid.js application. **Don't Do This:** Rely on outdated build processes or hand-rolled bundling solutions. **Why:** Modern build tools offer tree-shaking, code splitting, minification, and automatic handling of dependencies, leading to reduced bundle sizes and improved initial load times, crucial for Solid.js's performance. **Example (Vite):** """javascript // vite.config.js import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; export default defineConfig({ plugins: [solid()], build: { target: 'esnext', // Optimize for modern browsers minify: 'esbuild', // Use esbuild for faster and more efficient minification rollupOptions: { output: { manualChunks(id) { if (id.includes('node_modules')) { return 'vendor'; // Separate vendor dependencies } } } } } }); """ **Explanation:** * "vite-plugin-solid" integrates seamlessly with Solid.js projects. * "build.target: 'esnext'" targets modern browsers for optimal performance. * "build.minify: 'esbuild'" utilizes esbuild for fast minification. * "rollupOptions.output.manualChunks" enables code splitting, separating vendor dependencies into a "vendor" chunk for better caching. This allows browser to cache 3rd party libraries independently. ### 1.2. Configure Environment Variables **Do This:** Use environment variables for configuration settings that vary between environments (development, staging, production). **Don't Do This:** Hardcode sensitive information or environment-specific configurations directly in your source code. **Why:** Environment variables improve security, configurability, and portability, allowing you to easily adapt your application to different deployment environments. **Example:** """javascript // Accessing environment variables in Solid.js component import { createSignal, onMount } from 'solid-js'; function MyComponent() { const [apiUrl, setApiUrl] = createSignal(''); onMount(() => { // Access the API_URL environment variable const envApiUrl = import.meta.env.VITE_API_URL; setApiUrl(envApiUrl || 'default_api_url'); // Fallback if not defined }); return ( <div> API URL: {apiUrl()} </div> ); } export default MyComponent; // .env file VITE_API_URL=https://api.example.com """ **Explanation:** * Using "import.meta.env" (Vite's way of exposing env variables) to access environment variables at runtime. This works for other build tools like Rollup too, albeit with different env variable importing syntax. * Providing a default value as a fallback if the environment variable is not set. * Prefixing env variables with "VITE_" is required by vite. ### 1.3. Optimize Assets **Do This:** Optimize images, fonts, and other static assets before deployment. **Don't Do This:** Deploy large, unoptimized assets, which can significantly impact loading times. **Why:** Optimized assets reduce bandwidth consumption, improve loading speeds, and enhance the user experience. **Example:** * **Image Optimization:** Use tools like "imagemin" or online services like TinyPNG to compress images without significant loss of quality. * **Font Optimization:** Utilize font subsetting and modern font formats like WOFF2 to reduce font file sizes. * **Serving Compressed Assets:** Configure your server to serve assets with gzip or Brotli compression. ### 1.4. Enable Code Splitting **Do This:** Configure your build tool to perform code splitting, breaking your application into smaller chunks that can be loaded on demand. **Don't Do This:** Bundle your entire application into a single, monolithic JavaScript file. **Why:** Code splitting reduces initial load times by deferring the loading of non-critical code, improving the perceived performance of your Solid.js application. **Example (Vite Configuration - continued from above):** See section 1.1. for vite.config.js example. The "rollupOptions.output.manualChunks" configuration in that section enables code splitting. ### 1.5. Prerendering and SSR (Server-Side Rendering) **Do This:** Consider using Prerendering or Server-Side Rendering for parts of your application that need SEO or faster initial load times. SolidJS works well with tools like SolidStart for SSR. **Don't Do This:** Neglect optimizing for SEO if it's a requirement, or leave critical content rendered client-side when it could be prerendered. **Why:** Prerendering pre-builds static HTML pages for better SEO and faster initial load. SSR renders components on the server for improved performance on the first load, which is crucial for user experience and search engine crawlers. **Example (SolidStart):** """javascript // solid.config.js import solid from "solid-start/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [solid()], }); """ """javascript // app.jsx import { Routes, Route } from "@solidjs/router"; import Home from "./pages/Home"; import About from "./pages/About"; export default function App() { return ( <Routes> <Route path="/" component={Home} /> <Route path="/about" component={About} /> </Routes> ); } """ **Explanation:** - SolidStart simplifies SSR and prerendering for SolidJS applications. - "solid-start/vite" integrates SolidStart's features into the Vite build process. - You can define routes and components like a standard SolidJS app, and SolidStart handles the server-side rendering. ## 2. CI/CD Pipelines ### 2.1. Automate Builds and Deployments **Do This:** Implement a CI/CD pipeline using tools like GitHub Actions, GitLab CI, CircleCI, or Jenkins to automate the build, test, and deployment processes. **Don't Do This:** Manually build and deploy your application, which is error-prone and time-consuming. **Why:** CI/CD pipelines ensure consistent and reliable deployments, reduce the risk of human error, and allow for faster iteration cycles. **Example (GitHub Actions):** """yaml # .github/workflows/deploy.yml name: Deploy to Production on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm install - run: npm run build - name: Deploy to Firebase Hosting uses: w9jds/firebase-action@releases/v6 with: args: deploy --only hosting env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} """ **Explanation:** * This workflow is triggered when code is pushed to the "main" branch. * It sets up Node.js, installs dependencies, builds the Solid.js application, and deploys it to Firebase Hosting. * "secrets.FIREBASE_TOKEN" and "secrets.FIREBASE_PROJECT_ID" are stored securely in GitHub Secrets. ### 2.2. Implement Automated Testing **Do This:** Include automated tests (unit, integration, and end-to-end) in your CI/CD pipeline. **Don't Do This:** Deploy code without thorough testing, which can lead to bugs and regressions in production. **Why:** Automated tests ensure code quality, catch errors early, and prevent regressions, leading to a more stable and reliable application. **Example (Jest and Solid Testing Library):** """javascript // src/components/Counter.test.jsx import { render, fireEvent } from '@solidjs/testing-library'; import Counter from './Counter'; describe('Counter Component', () => { it('increments the counter when the button is clicked', async () => { const { getByText } = render(() => <Counter />); const incrementButton = getByText('Increment'); const counterValue = getByText('0'); // Assuming initial value is 0. fireEvent.click(incrementButton); await (() => expect(getByText('1')).toBeVisible()); fireEvent.click(incrementButton); await (() => expect(getByText('2')).toBeVisible()); }); }); """ **Explanation:** * Uses "@solidjs/testing-library" for rendering and interacting with Solid.js components. * "render" mounts the component for testing. * "fireEvent.click" simulates a button click. * "getByText" retrieves elements based on their text content. * The test asserts that the counter value increments correctly after each click. ### 2.3. Use Version Control **Do This:** Use a version control system like Git to track changes to your codebase. **Don't Do This:** Directly modify production code without proper version control. **Why:** Version control enables collaboration, allows you to revert to previous versions, and provides a history of changes, which is essential for managing complex projects. ### 2.4. Implement Feature Flags **Do This:** Use feature flags to control the release of new features to a subset of users or to enable/disable features without deploying new code. **Don't Do This:** Deploy new features directly to all users without proper testing or monitoring. **Why:** Feature flags reduce the risk of introducing bugs or performance issues to all users at once. **Example (using a basic feature flag in a Solid component):** """jsx import { createSignal } from 'solid-js'; const FEATURE_FLAG_NEW_UI = import.meta.env.VITE_FEATURE_NEW_UI === 'true'; function MyComponent() { return ( <div> {FEATURE_FLAG_NEW_UI ? ( <p>New UI is enabled!</p> ) : ( <p>Old UI is active.</p> )} </div> ); } export default MyComponent; // .env VITE_FEATURE_NEW_UI=true """ **Explanation** * We read a flag from the environment ("VITE_FEATURE_NEW_UI"). Note the "=== 'true'" comparison is important, since env variables are strings. * We use a ternary operator to conditionally render different UI elements depending on whether the flag is enabled or not. ## 3. Production Environment ### 3.1. Use a CDN **Do This:** Serve static assets (JavaScript, CSS, images) from a Content Delivery Network (CDN). **Don't Do This:** Serve static assets directly from your origin server, which can increase latency and reduce performance. **Why:** CDNs distribute your content across multiple servers globally, reducing latency and improving loading speeds for users around the world. ### 3.2. Configure Caching **Do This:** Configure appropriate caching headers (e.g., "Cache-Control", "Expires") for static assets and API responses. **Don't Do This:** Use overly aggressive or ineffective caching strategies, which can lead to stale content or unnecessary requests. **Why:** Caching reduces the load on your server, improves response times, and enhances the user experience. ### 3.3. Monitor Performance and Errors **Do This:** Implement monitoring and error tracking using tools like Sentry, New Relic, or Datadog. **Don't Do This:** Deploy code without proper monitoring, which makes it difficult to identify and resolve issues in production. **Why:** Monitoring and error tracking provide insights into the performance and stability of your application, allowing you to proactively identify and resolve issues. **Example (Sentry):** """javascript // src/index.jsx or main.jsx import * as Sentry from "@sentry/solid"; Sentry.init({ dsn: "YOUR_SENTRY_DSN", integrations: [ new Sentry.BrowserTracing(), new Sentry.Replay() ], // Performance Monitoring tracesSampleRate: 0.1, // Capture 10% of transactions for performance traces. // Session Replay replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to lower it at first. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. }); import App from './App'; import { render } from 'solid-js/web'; render(() => <App />, document.getElementById('root')); """ **Explanation:** * "Sentry.init" initializes Sentry with your DSN (Data Source Name). * "BrowserTracing" integration enables performance monitoring. * "Replay" integration enables session replay features to visually see what the user experienced when an error occurred. * "tracesSampleRate" controls the sampling rate for performance traces. * "replaysSessionSampleRate" controls the sampling rate for session replays. * "replaysOnErrorSampleRate" allows you to capture replays specifically when errors occur. * Wrap the application rendering in "Sentry.withProfiler" to profile Solid.js components. ### 3.4. Implement Logging **Do This:** Implement structured logging throughout your application. **Don't Do This:** Rely solely on "console.log" statements, which are difficult to manage in production. **Why:** Structured logging provides valuable context for debugging and troubleshooting issues in production. **Example:** """javascript import log from 'loglevel'; // Use a logging library like loglevel log.setLevel(log.levels.DEBUG); // Set the log level based on environment function MyComponent() { return( <button onClick={() => { log.debug("Button clicked"); }}> Click me </button> ); } export default MyComponent; """ ### 3.5. Secure Your Application **Do This:** Implement security best practices, such as input validation, output encoding, and protection against common web vulnerabilities like XSS and CSRF. **Don't Do This:** Neglect security considerations, which can leave your application vulnerable to attacks. **Why:** Security is essential for protecting user data and preventing malicious activity. Solid.js is not immune to common WebApp vulnerabilities and should be assessed regularly. ### 3.6. Proper Error Handling **Do This:** Implement global error boundaries using Solid's "<ErrorBoundary>" component and proactively catch errors to prevent application crashes. **Don't Do This:** Allow unhandled exceptions to propagate, leading to a broken user experience. **Why:** Error boundaries and proactive error handling improve the resilience of your application by gracefully handling unexpected errors. **Example:** """jsx import { ErrorBoundary } from 'solid-js'; import MyComponent from './MyComponent'; function App() { return ( <ErrorBoundary fallback={error => <p>An error occurred: {error.message}</p>}> <MyComponent /> </ErrorBoundary> ); } export default App; """ ## 4. Solid.js Specific Considerations ### 4.1. Leverage Solid.js's Reactive Nature **Do This:** Understand and leverage Solid.js's reactive primitives effectively in your build and deployment setup. For example, optimize data fetching and updates to minimize unnecessary re-renders. **Don't Do This:** Treat Solid.js like other virtual DOM libraries (like react). **Why:** Solid.js's fine-grained reactivity allows for precise updates, which can significantly improve performance when data changes frequently. ### 4.2 Hydration considerations in SolidJS **Do This:** If using SSR, pay careful attention to hydration optimization when the server-rendered content loads client-side. **Don't Do This:** Neglect hydration and cause full re-renders. Ensure data consistency to prevent hydration mismatches. **Why:** Efficient hydration is crucial for optimal SSR performance. Avoid unnecessary re-renders and data mismatches that harm the user experience. ### 4.3 Component Library Versioning **Do This:** Use Semantic Versioning (SemVer) and clear commit messages for any SolidJS component library to ensure that consumers of your components can safely upgrade, or explicitly understand the updates they're getting. **Don't Do This:** Make breaking changes in minor or patch releases to component libraries as this can affect many consumers. **Why:** Managing versions correctly, especially in component libraries, promotes maintainability, reduces dependency conflicts, and eases integrations for other SolidJS projects. ## 5. Security Considerations Specific to Solid.js * **Avoid Insecurely Rendered Content**: Like all frontend frameworks, be careful when rendering user-provided content that is not properly sanitized. Use appropriate escaping techniques and consider libraries like DOMPurify to prevent XSS attacks. * **Secure API Endpoints**: While not specific to Solid.js but a general security practice, ensure that all your backend API endpoints that your Solid.js applications interact with are secure and follow security best practices. * **Dependency Management**: Regularly audit your dependencies for known vulnerabilities using tools like "npm audit" or "yarn audit". * **Secrets Management**: Use environment variables and secure storage solutions to manage API keys, tokens, and other sensitive information. Never commit sensitive information directly to your codebase. * **Rate Limiting**: Implement rate limiting on your backend APIs to protect against abuse and DoS attacks. * **Content Security Policy (CSP)**: Configure a strong Content Security Policy (CSP) to prevent XSS attacks by controlling the resources that the browser is allowed to load. * **Subresource Integrity (SRI)**: Use SRI to ensure that your application loads unmodified versions of third-party resources from CDNs. * **Regular Security Audits**: Conduct regular security audits and penetration testing to identify and address potential vulnerabilities in your application. These standards provide a comprehensive guide for deploying and managing Solid.js applications, focusing on performance, reliability, and security. Adhering to these practices will help you build and maintain robust Solid.js applications that deliver a great user experience.
# Security Best Practices Standards for Solid.js This document outlines security best practices for Solid.js development. Following these guidelines will help protect applications against common vulnerabilities, implement secure coding patterns, and maintain a robust security posture. These standards are tailored for Solid.js and leverage the latest features of the framework. ## 1. Input Validation and Sanitization ### 1.1. Standard: Validate and sanitize all user inputs. **Why:** Input validation and sanitization are crucial to prevent injection attacks (e.g., XSS, SQL injection) and ensure data integrity. Solid.js applications often interact with user-provided data through forms, APIs, and other sources. **Do This:** * Use appropriate validation libraries or custom validation functions to verify that the input data conforms to expected formats, types, and constraints. * Sanitize input data to remove or escape potentially harmful characters or code. **Don't Do This:** * Directly use user-provided data in your application without validation or sanitization. * Rely solely on client-side validation, as it can be bypassed. **Example:** """jsx import { createSignal } from 'solid-js'; function InputValidation() { const [inputValue, setInputValue] = createSignal(''); const [sanitizedValue, setSanitizedValue] = createSignal(''); const handleInputChange = (event) => { const rawInput = event.target.value; setInputValue(rawInput); // Sanitize input using a simple escaping function const sanitized = escapeHTML(rawInput); setSanitizedValue(sanitized); }; // Simple HTML escaping function const escapeHTML = (str) => { let div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; }; return ( <div> <label htmlFor="userInput">Enter some text:</label> <input type="text" id="userInput" value={inputValue()} onChange={handleInputChange} /> <p>Original Input: {inputValue()}</p> <p>Sanitized Output: {sanitizedValue()}</p> </div> ); } export default InputValidation; """ **Explanation:** * The "escapeHTML" function demonstrates a basic sanitization technique. More robust libraries like DOMPurify should be favored in production. The important idea here is that the value being rendered in the Solid component is sanitized before being rendered to the DOM. * This simple example shows how input can be sanitized immediately upon change, ensuring displayed and processed data is safe. ### 1.2. Standard: Server-side validation for critical data. **Why:** Client-side validation can be bypassed, making server-side validation essential for data integrity and security. **Do This:** * Implement server-side validation for all data that is critical to the application's functionality or security. * Return clear and informative error messages to the client if validation fails. **Don't Do This:** * Trust client-side validation as the sole source of data validation. * Expose sensitive server-side validation logic to the client. **Example:** """javascript // Server-side (Node.js with Express) app.post('/api/submit', (req, res) => { const { name, email } = req.body; // Server-side validation if (!name || name.length < 3) { return res.status(400).json({ error: 'Name is required and must be at least 3 characters long.' }); } if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return res.status(400).json({ error: 'Invalid email format.' }); } // Process valid data (e.g., save to database) // ... res.status(200).json({ message: 'Data submitted successfully.' }); }); """ **Explanation:** This example demonstrates server-side validation performed in a Node.js with Express application. The server validates the "name" and "email" fields and returns an error response if the validation fails. ## 2. Authentication and Authorization ### 2.1. Standard: Implement secure authentication mechanisms. **Why:** Secure authentication is vital to verify the identity of users and protect sensitive data. **Do This:** * Use established authentication protocols like OAuth 2.0, OpenID Connect, or SAML for user authentication. * Store passwords securely using strong hashing algorithms (e.g., bcrypt, Argon2). * Implement multi-factor authentication (MFA) for enhanced security. * Protect API endpoints with appropriate authentication and authorization mechanisms. **Don't Do This:** * Store passwords in plain text or using weak hashing algorithms. * Expose sensitive credentials in client-side code. * Bypass authentication checks or rely on insecure methods. **Example:** Using a library like "solid-auth-client" to handle authentication: """jsx import { createSignal, createEffect } from 'solid-js'; import { signIn, signOut, fetch, useSession } from 'solid-auth-client'; function AuthComponent() { const [user, setUser] = createSignal(null); const [session] = useSession(); createEffect(() => { if(session.state === "authenticated"){ setUser(session.info.user); } else { setUser(null); } }); const handleSignIn = async () => { await signIn({ provider: 'google' }); // or any other provider }; const handleSignOut = async () => { await signOut(); }; return ( <div> {user() ? ( <div> <p>Logged in as: {user().name}</p> <button onClick={handleSignOut}>Sign Out</button> </div> ) : ( <button onClick={handleSignIn}>Sign In with Google</button> )} </div> ); } export default AuthComponent; """ **Explanation:** * The "solid-auth-client" library simplifies the authentication process by providing hooks and functions for signing in, signing out, and managing user sessions. * This example integrates with Google for authentication, but the library supports multiple providers. The core idea is to abstract away the complexities of the OAuth flow or other authentication mechanism. ### 2.2. Standard: Implement robust authorization controls. **Why:** Authorization ensures that users have appropriate access levels to resources and functionalities. **Do This:** * Define clear roles and permissions for different user groups. * Enforce authorization checks at the server-side to prevent unauthorized access. * Use access control lists (ACLs) or role-based access control (RBAC) to manage permissions. **Don't Do This:** * Grant excessive permissions to users. * Rely solely on client-side authorization checks. * Expose sensitive data or functionalities without authorization checks. **Example:** """javascript // Server-side (Node.js with Express) // Middleware to check user role const checkRole = (role) => (req, res, next) => { const userRole = req.user.role; // Assuming user role is available in req.user if (userRole === role) { next(); // User has the required role, proceed to the next middleware/route handler } else { res.status(403).json({ error: 'Unauthorized: Insufficient permissions.' }); // User does not have the required role } }; // Route requiring admin role app.get('/admin/dashboard', checkRole('admin'), (req, res) => { res.status(200).json({ message: 'Admin Dashboard Content' }); }); // Route requiring editor role app.get('/editor/content', checkRole('editor'), (req, res) => { res.status(200).json({ message: 'Editor Content' }); }); """ **Explanation:** This example demonstrates role-based authorization using middleware in a Node.js with Express application. The "checkRole" middleware verifies that the user has the required role before granting access to specific routes. ## 3. Cross-Site Scripting (XSS) Prevention ### 3.1. Standard: Properly escape or sanitize output data. **Why:** XSS attacks can occur when untrusted data is rendered in the application's output without proper escaping or sanitization. **Do This:** * Use Solid.js's built-in escaping mechanisms to prevent XSS attacks. Solid's reactivity model automatically escapes values being passed to the DOM so this reduces the burden but does not eliminate completely. Sanitization is still required for un-escaped contexts. * Use trusted libraries (e.g., DOMPurify) to sanitize HTML content. **Don't Do This:** * Render untrusted data directly without escaping or sanitization in situations where the escape context is incorrect. * Disable XSS protection mechanisms. **Example:** """jsx import { createSignal } from 'solid-js'; import DOMPurify from 'dompurify'; function XSSPrevention() { const [userInput, setUserInput] = createSignal(''); const [safeHTML, setSafeHTML] = createSignal(''); const handleInputChange = (event) => { const rawInput = event.target.value; setUserInput(rawInput); // Sanitize HTML using DOMPurify const sanitizedHTML = DOMPurify.sanitize(rawInput); setSafeHTML(sanitizedHTML); }; return ( <div> <label htmlFor="xssInput">Enter some HTML:</label> <input type="text" id="xssInput" value={userInput()} onChange={handleInputChange} /> <p>Original Input: {userInput()}</p> <p>Sanitized Output: <div innerHTML={safeHTML()}></div></p> </div> ); } export default XSSPrevention; """ **Explanation:** * The "DOMPurify.sanitize" function sanitizes the user-provided HTML to prevent XSS attacks. * The sanitized HTML is then rendered using "innerHTML", which is properly escaped. ### 3.2. Standard: Use Content Security Policy (CSP). **Why:** CSP is an HTTP header that allows you to control the sources from which the browser is allowed to load resources, reducing the risk of XSS attacks. **Do This:** * Configure CSP headers on your server to restrict the sources of scripts, stylesheets, images, and other resources. * Use a strict CSP policy that only allows resources from trusted origins. **Don't Do This:** * Use a permissive CSP policy that allows resources from any origin. * Inline JavaScript code, as it can bypass CSP policies. **Example:** """ // Server-side (Node.js with Express) app.use((req, res, next) => { res.setHeader( 'Content-Security-Policy', "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' https://trusted-cdn.com; img-src 'self' data:; font-src 'self';" ); next(); }); """ **Explanation:** This example sets the "Content-Security-Policy" header in a Node.js with Express application. The policy restricts scripts and stylesheets to the application's origin and "https://trusted-cdn.com", images to the application's origin and data URIs, and fonts to the application's origin. ## 4. Cross-Site Request Forgery (CSRF) Prevention ### 4.1. Standard: Implement CSRF protection mechanisms. **Why:** CSRF attacks occur when an attacker tricks a user into performing actions on a website without their knowledge or consent. **Do This:** * Use CSRF tokens to protect state-changing requests (e.g., form submissions, API calls). * Implement the "SameSite" cookie attribute to prevent cross-site cookie sharing. **Don't Do This:** * Rely solely on cookies for authentication, as they can be targeted by CSRF attacks. * Disable CSRF protection mechanisms. **Example:** """javascript // Server-side (Node.js with Express) const csrf = require('csurf'); const cookieParser = require('cookie-parser'); app.use(cookieParser()); app.use(csrf({ cookie: true })); app.get('/form', (req, res) => { // Pass the CSRF token to the client-side res.json({ csrfToken: req.csrfToken() }); }); app.post('/submit', (req, res) => { // CSRF token is automatically validated by the csrf middleware // Process the request res.status(200).json({ message: 'Form submitted securely.' }); }); // Client-side (Solid.js) import { createSignal, createEffect } from 'solid-js'; import { createResource } from 'solid-js'; function CSRFExample() { const [csrfTokenFetcher] = createResource('/form', async () => { const res = await fetch('/form'); return res.json(); }) const handleSubmit = async (event) => { event.preventDefault(); const data = new FormData(event.target); const csrfToken = csrfTokenFetcher.value?.csrfToken; data.append("_csrf", csrfToken); const response = await fetch('/submit', { method: 'POST', body: data }); const result = await response.json(); console.log(result); }; return ( <form onSubmit={handleSubmit}> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" required /> <button type="submit">Submit</button> </form> ); } export default CSRFExample; """ **Explanation:** * The server generates a CSRF token on the server and sends it to the client. * The client includes the CSRF token in the request body for state changing events. * The server validates the CSRF token to prevent CSRF attacks. ## 5. Data Security and Privacy ### 5.1. Standard: Protect sensitive data. **Why:** Data breaches can have severe consequences, including financial losses, reputational damage, and legal liabilities. **Do This:** * Encrypt sensitive data both in transit and at rest. * Use HTTPS to encrypt communication between the client and the server. * Store sensitive data securely using encryption keys and access controls. * Implement data masking or redaction techniques to protect sensitive data in logs and reports. **Don't Do This:** * Store sensitive data in plain text. * Expose encryption keys in client-side code. * Log sensitive data without proper redaction. ### 5.2. Standard: Implement data privacy measures. **Why:** Data privacy is essential to comply with regulations like GDPR and CCPA and to build trust with users. **Do This:** * Collect only the data that is necessary for the intended purpose. * Obtain user consent before collecting and processing personal data. * Provide users with the ability to access, correct, and delete their personal data. * Implement data retention policies to ensure that data is not stored longer than necessary. **Don't Do This:** * Collect excessive amounts of personal data. * Use personal data for purposes that are not disclosed to users. * Store personal data indefinitely. ## 6. Dependency Management ### 6.1. Standard: Keep dependencies up to date. **Why:** Outdated dependencies may contain known vulnerabilities that can be exploited by attackers. **Do This:** * Regularly update dependencies to the latest stable versions. * Use dependency management tools (e.g., npm, yarn, pnpm) to track and update dependencies but audit those tools themselves also. * Monitor dependency vulnerability databases (e.g., npm audit, Snyk) for known vulnerabilities. **Don't Do This:** * Use outdated dependencies without regular updates. * Ignore security warnings from dependency management tools. """bash npm audit """ **Explanation:** This command scans the project dependencies for known vulnerabilities and provides recommendations for remediation. ## 7. Error Handling and Logging ### 7.1. Standard: Implement secure error handling. **Why:** Detailed error messages can expose sensitive information or reveal vulnerabilities. **Do This:** * Return generic error messages to the client to avoid disclosing sensitive information. * Log detailed error messages on the server-side for debugging purposes. * Implement rate limiting to prevent denial-of-service attacks. **Don't Do This:** * Expose detailed error messages to the client. * Log sensitive information in error messages. ### 7.2. Standard: Implement comprehensive logging. **Why:** Logging is essential for monitoring application behavior, detecting security incidents, and troubleshooting issues. **Do This:** * Log all significant events, including user actions, authentication attempts, and error conditions. * Use a centralized logging system to collect and analyze logs. * Implement log rotation to prevent logs from consuming excessive disk space. * Securely store and protect log data from unauthorized access. **Don't Do This:** * Disable logging or log only minimal information. * Store logs in plain text without access controls. * Log sensitive data without proper redaction. ## 8. Code Review and Testing ### 8.1. Standard: Conduct regular code reviews. **Why:** Code reviews can help identify security vulnerabilities and other issues before they make it into production. **Do This:** * Establish a code review process that involves multiple reviewers. * Use automated code analysis tools to detect potential security vulnerabilities. * Focus on security-related aspects during code reviews, such as input validation, authorization checks, and data protection. **Don't Do This:** * Skip code reviews or assign them to inexperienced developers. * Ignore security warnings from code analysis tools. ### 8.2. Standard: Implement security testing. **Why:** Security testing can help identify vulnerabilities and weaknesses in the application's security posture. **Do This:** * Conduct regular penetration testing and vulnerability scanning. * Implement automated security testing as part of the CI/CD pipeline. * Test for common vulnerabilities, such as XSS, CSRF, SQL injection, and authentication flaws. * Remediate identified vulnerabilities promptly. **Don't Do This:** * Skip security testing or conduct it infrequently. * Ignore identified vulnerabilities or defer remediation. ## 9. Solid.js Specific Security Considerations ### 9.1. Standard: Secure Server-Side Rendering (SSR). **Why:** If you use Solid.js with server-side rendering, be especially careful about XSS vulnerabilities, as the server is generating HTML that will be directly rendered by the client. A misconfigured server can easily lead to XSS. **Do This:** * Sanitize all data before rendering on the server. * Use a templating engine that automatically escapes HTML. * Ensure all server dependencies are up-to-date. **Don't Do This:** * Trust that the server's environment is inherently safe. ### 9.2. Standard: Protect against prototype pollution. **Why:** Prototype pollution is a JavaScript vulnerability where attackers can modify the properties of JavaScript object prototypes, potentially leading to XSS or other unexpected behaviors, including denial of service. **Do This:** * Avoid using libraries or functions that perform deep merges or cloning of objects without proper sanitization. * If you must use such functions, carefully validate and sanitize all input data. **Don't Do This:** * Blindly use deep merge or clone functions on untrusted data. ### 9.3. Standard: Monitor third-party Solid.js libraries. **Why:** Solid.js benefits from a rich ecosystem of third-party libraries. However, these libraries can introduce security vulnerabilities if not properly vetted or maintained. **Do This:** * Thoroughly vet all third-party libraries before including them in your project. * Check for known security vulnerabilities and ensure the library is actively maintained. * Regularly update libraries to patch any security issues. **Don't Do This:** * Blindly trust all third-party libraries without proper vetting. * Use outdated and unmaintained libraries. By adhering to these security best practices, Solid.js developers can build more secure and robust applications. Remember that security is an ongoing process that requires continuous vigilance and improvement.