# Core Architecture Standards for Solid.js
This document outlines the core architectural standards for Solid.js projects. It provides guidelines for structuring applications, managing state, organizing components, and leveraging Solid.js's reactivity model efficiently. These standards are designed to promote maintainability, readability, performance, and scalability in your Solid.js applications.
## 1. Project Structure and Organization
A well-defined project structure is crucial for maintainability, especially as the application grows.
**Standard:** Organize your project using a feature-based or domain-based structure, grouping related components, utilities, and styles together.
**Why:** This approach increases cohesion and reduces coupling, making it easier to locate, understand, and modify code related to a specific feature.
**Do This:**
"""
src/
components/
FeatureA/
FeatureA.tsx // Main component
FeatureA.module.css // Component-specific styles (CSS Modules)
FeatureA.spec.tsx // Unit tests
utils.ts // Utility functions specific to FeatureA
FeatureB/
...
contexts/ // Global state management with contexts
AppContext.tsx
hooks/ // Custom hooks for reusable logic
useDataFetching.ts
pages/ // Top-level route components/pages
HomePage.tsx
AboutPage.tsx
services/ // API communication modules
api.ts
styles/ // Global styles
global.css
utils/ // General utility functions
dateUtils.ts
formatUtils.ts
App.tsx // Root component
index.tsx // Entry point
vite-env.d.ts
"""
**Don't Do This:**
"""
src/
components/ // Unorganized dump of all components
ComponentA.tsx
ComponentB.tsx
...
utils/ // Unrelated utility functions mixed together
api.ts
dateUtils.ts
formatUtils.ts
App.tsx
index.tsx
"""
**Explanation:** The recommended structure promotes modularity. Feature-specific logic resides within its folder, keeping individual components lean and focused. Placing pages under "/pages" clarifies the routing structure of the application. The "/services" directory encapsulates API interactions. Use CSS modules to scope styles to individual components, avoiding naming conflicts.
**Code Example:**
"""typescript
// src/components/FeatureA/FeatureA.tsx
import styles from './FeatureA.module.css'; // CSS Modules
interface FeatureAProps {
data: string;
}
function FeatureA(props: FeatureAProps) {
return (
{/* Access styles using styles.className */}
<p>Feature A: {props.data}</p>
);
}
export default FeatureA;
// src/components/FeatureA/FeatureA.module.css
.container {
background-color: #f0f0f0;
padding: 10px;
border: 1px solid #ccc;
}
"""
## 2. Component Architecture
Solid.js components should be small, focused, and reusable. Leverage the reactivity system for efficient updates.
**Standard:** Create functional components that are reactive and focused on a single responsibility. Prefer composition over inheritance.
**Why:** Functional components improve code clarity and testability. Composition allows you to build complex UIs by combining smaller, reusable components.
**Do This:**
"""typescript
// Small, focused component
interface ButtonProps {
onClick: () => void;
children: any;
}
function Button(props: ButtonProps) {
return {props.children};
}
"""
"""typescript
// Composing components
import Button from './Button';
interface CardProps {
title: string;
description: string;
}
function Card(props: CardProps) {
return (
{props.title}
<p>{props.description}</p>
alert('Clicked!')}>Learn More
);
}
"""
**Don't Do This:**
* Creating monolithic components with too much logic.
* Using class components (functional components are preferred in Solid.js).
* Relying on inheritance.
**Explanation:** Solid.js favors functional components due to their simplicity and performance characteristics. Composition promotes reusability and flexibility. Keep components small and focused on a single task to improve maintainability.
**Code Example:**
"""typescript
// Example using Solid's reactivity
import { createSignal, JSX } from 'solid-js';
function Counter(): JSX.Element {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
return (
<p>Count: {count()}</p>
Increment
);
}
export default Counter;
"""
## 3. State Management
Choose a state management strategy that aligns with the complexity of your application. Solid.js provides built-in primitives, but for larger applications, consider libraries like Zustand or Jotai.
**Standard:** Use "createSignal" for local component state. Consider using contexts for application-wide state or specialized state management libraries like Zustand or Jotai for more complex needs.
**Why:** "createSignal" is the fundamental reactive primitive in Solid.js. Contexts provide a simple way to share state across components. Zustand and Jotai offer scalability and performance optimizations for large applications.
**Do This:**
"""typescript
// Local state with createSignal
import { createSignal } from 'solid-js';
function MyComponent() {
const [name, setName] = createSignal('Initial Name');
const updateName = (newName: string) => {
setName(newName);
};
return (
<p>Name: {name()}</p>
updateName(e.currentTarget.value)} />
);
}
"""
"""typescript
// Global state with context
import { createContext, useContext, createSignal, JSX } from 'solid-js';
interface AppContextType {
theme: [string, (theme: string) => void];
}
const AppContext = createContext({} as AppContextType);
interface AppProviderProps {
children: JSX.Element
}
function AppProvider(props: AppProviderProps) {
const theme = createSignal('light');
return (
{props.children}
);
}
function useApp() {
return useContext(AppContext);
}
import { ParentComponent } from './ParentComponent';
function App() {
return (
);
}
export default App;
//Example usage inside ParentComponent.tsx
import { useApp } from '../AppContext';
function ParentComponent() {
const {theme} = useApp();
return(
<>
Value via context: {theme[0]()}
)
}
"""
"""typescript
// Example Zustand Usage
import create from 'zustand';
interface BearState {
bears: number;
increase: (by: number) => void;
}
const useBearStore = create((set) => ({
bears: 0,
increase: (by: number) => set((state) => ({ bears: state.bears + by })),
}));
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return {bears} around here ...;
}
function IncreasePopulation() {
const increase = useBearStore((state) => state.increase);
return increase(1)}>one up;
}
function App() {
return (
<>
);
}
"""
**Don't Do This:**
* Overusing global state for component-specific data.
* Mutating state directly without using setter functions from "createSignal".
* Ignoring the performance implications of excessive state updates.
**Explanation:** Choose the state management strategy based on the scale and complexity of the application. "createSignal" is ideal for local state, while contexts and Zustand are beneficial for managing global state and side effects. Avoid direct mutation of signal values, always use the setter function to trigger reactivity.
**Code Example:**
"""typescript
// Correct way to update a signal
import { createSignal } from 'solid-js';
function UpdateSignalExample() {
const [count, setCount] = createSignal(0);
const increment = () => {
setCount(count() + 1); // Correct way to update
};
return (
<p>Count: {count()}</p>
Increment
);
}
"""
## 4. Asynchronous Operations and Data Fetching
Handle asynchronous operations gracefully using asynchronous functions and error handling. Solid.js provides tools like "createResource" for managing data fetching.
**Standard:** Use "createResource" for data fetching. Handle loading and error states appropriately. Encapsulate API calls in dedicated services.
**Why:** "createResource" simplifies data fetching, automatically manages loading and error states, and integrates seamlessly with Solid's reactivity system. Dedicated services improve code organization and reusability.
**Do This:**
"""typescript
// Using createResource for data fetching
import { createResource } from 'solid-js';
interface Post {
id: number;
title: string;
body: string;
}
async function fetchPosts(): Promise {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
}
function PostsList() {
const [posts, { mutate, refetch }] = createResource(fetchPosts);
return (
{posts.loading ? (
<p>Loading...</p>
) : posts.error ? (
<p>Error: {posts.error.message}</p>
) : (
{posts().map((post) => (
{post.title}
))}
)}
Refresh
);
}
"""
"""typescript
// Encapsulating API calls in a service
// src/services/api.ts
async function getPosts(): Promise {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
}
export const api = {
getPosts,
};
// Usage in a component:
import { createResource } from 'solid-js';
import { api } from '../services/api';
function PostsList() {
const [posts, { mutate, refetch }] = createResource(api.getPosts);
// ... rest of the component
}
"""
**Don't Do This:**
* Fetching data directly in components without handling loading/error states.
* Repeating API calls in multiple components.
* Ignoring error handling during API requests.
**Explanation:** "createResource" provides a declarative way to handle asynchronous data fetching. Encapsulate API interactions within a service layer to avoid code duplication and promote separation of concerns. Ensure robust error handling to provide a better user experience.
**Code Example:**
"""typescript
// Handling data fetching with error boundary
import { createResource, ErrorBoundary } from 'solid-js';
interface Post {
id: number;
title: string;
body: string;
}
async function fetchPosts(): Promise {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error("Failed to fetch posts");
}
return response.json();
}
function PostsList() {
const [posts] = createResource(fetchPosts);
return (
{posts()?.map((post) => (
{post.title}
))}
);
}
function App() {
return (
<p>Something went wrong: {error.message}</p>}>
);
}
export default App;
"""
## 5. Reactivity and Performance
Solid.js's reactivity model is highly efficient. Understand how to leverage it effectively to avoid unnecessary re-renders and optimize performance.
**Standard:** Optimize reactivity by creating signals only when necessary. Avoid unnecessary computations within reactive contexts. Use "createMemo" for derived values. Use "untrack" to read signals without creating a dependency.
**Why:** Efficient reactivity leads to faster updates and better overall application performance. "createMemo" helps avoid redundant computations. "untrack" provides fine-grained control over dependency tracking.
**Do This:**
"""typescript
// Using createMemo for derived values
import { createSignal, createMemo } from 'solid-js';
function FullNameComponent() {
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
const fullName = createMemo(() => "${firstName()} ${lastName()}"); // Only updates when firstName or lastName change
return (
<p>Full Name: {fullName()}</p>
setFirstName(e.currentTarget.value)} />
setLastName(e.currentTarget.value)} />
);
}
"""
"""typescript
// Using untrack to avoid unnecessary dependencies
import { createSignal, untrack } from 'solid-js';
function UntrackExample() {
const [count, setCount] = createSignal(0);
const [message, setMessage] = createSignal('Initial Message');
const handleClick = () => {
untrack(() => { // Read count without creating a dependency
console.log("Button clicked. Current count: ${count()}");
});
setMessage('Button was clicked!');
};
return (
<p>Count: {count()}</p>
<p>Message: {message()}</p>
Click me
);
}
"""
**Don't Do This:**
* Performing expensive calculations directly within component render functions.
* Creating signals unnecessarily.
* Ignoring potential performance bottlenecks caused by excessive re-renders.
**Explanation:** "createMemo" helps to avoid redundant computations by caching the result and only re-evaluating when its dependencies change. "untrack" lets you read signals without causing side effects. These tools enhance the performance of your applications.
**Code Example:**
"""typescript
// Optimizing list rendering using keyed list
import { createSignal, For } from 'solid-js';
interface Item {
id: number;
text: string;
}
function KeyedListExample() {
const [items, setItems] = createSignal([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
]);
const addItem = () => {
setItems([...items(), { id: Date.now(), text: "New Item ${Date.now()}" }]);
};
return (
Add Item
item.id}>
{(item) => {item.text}}
);
}
"""
## 6. Error Handling
Robust error handling is important for creating reliable applications.
**Standard:** Use "ErrorBoundary" components to catch errors gracefully. Implement centralized error logging. Provide user-friendly error messages.
**Why:** "ErrorBoundary" provides a declarative way to handle errors within components. Centralized logging helps track and debug errors. User-friendly messages improve the user experience.
**Do This:**
"""typescript
// Using ErrorBoundary
import { ErrorBoundary } from 'solid-js';
function MyComponent() {
throw new Error('Something went wrong!'); // Simulate an error
return <p>This will not be rendered.</p>;
}
"""
"""typescript
function App() {
return (
<p>An error occurred: {error.message}</p>}>
);
}
export default App;
"""
**Don't Do This:**
* Ignoring errors.
* Displaying technical error details to users.
* Failing to log errors for debugging.
**Explanation:** "ErrorBoundary" prevents component errors from crashing the entire application. Ensure error messages are displayed correctly and don't expose sensitive information.
**Code Example:**
Implement an ErrorLogger service for error logging.
"""typescript
class ErrorLogger {
static log(error: any, componentStack?: string) {
console.error('Error', error);
if (componentStack) {
console.error('Component Stack', componentStack)
}
//Optional: Send the error to a server-side logging service such as Sentry, LogRocket, etc.
//Example
//Sentry.captureException(error, {
// contexts: {
// react: {componentStack},
// },
//});
}
}
export default ErrorLogger;
"""
## 7. Code Style and Formatting
Maintain consistent code style and formatting for readability and maintainability.
**Standard:** Use Prettier and ESLint with configured Solid.js-specific rules. Enforce consistent naming conventions for variables and components. Add comments and documentation to explain complex logic.
**Why:** Consistent code style improves readability and reduces the cognitive load for developers. Linting tools help catch potential errors early.
**Do This:**
* Configure Prettier and ESLint specifically for Solid.js projects.
* Use descriptive variable and function names.
* Write concise and informative comments.
"""json
// .prettierrc.json
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
"""
"""
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'solid'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:solid/recommended',
'prettier', // Make sure "prettier" is the last element in this array.
],
rules: {
// Add custom rules here
},
};
"""
**Don't Do This:**
* Ignoring code style guidelines.
* Using inconsistent naming conventions.
* Writing complex code without comments or documentation.
**Explanation:** Tools like Prettier and ESLint help automate code formatting and catch potential errors. Consistency results in more understandable and maintainable code.
**Code Example:**
"""typescript
// Example of a well-documented function
/**
* Formats a date string into a user-friendly format.
* @param dateString The date string to format.
* @returns A formatted date string.
* @throws Error if the date string is invalid.
*/
function formatDate(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleDateString(); // Format the date
} catch (error) {
console.error('Invalid date string:', dateString);
throw new Error('Invalid date string provided.');
}
}
"""
## 8. Testing
Write comprehensive tests to ensure the reliability of your application.
**Standard:** Use Jest or Vitest for unit and integration tests. Aim for high test coverage, particularly in critical code paths. Write tests that are readable, maintainable, and focused on testing specific behavior.
**Why:** Testing ensures that the application functions as expected and reduces the risk of introducing bugs.
**Do This:**
* Write unit tests for individual components and functions.
* Write integration tests for testing interactions between components.
* Use mocking to isolate components during testing.
"""typescript
// Example unit test using Vitest
import { describe, it, expect } from 'vitest';
import { render, fireEvent } from '@solidjs/testing-library';
import Counter from './Counter';
describe('Counter Component', () => {
it('should increment the count when the button is clicked', async () => {
const { getByText } = render(() => );
const incrementButton = getByText('Increment');
const countElement = getByText('Count: 0');
await fireEvent.click(incrementButton);
expect(getByText('Count: 1')).toBeDefined();
});
});
"""
**Don't Do This:**
* Skipping tests for critical functionality.
* Writing tests that are tightly coupled to implementation details.
* Ignoring test failures.
**Explanation:** Testing is paramount for writing robust, maintainable and reliable code. Focus on writing tests that target specific behaviors to prevent regressions.
By adhering to these architectural standards, your Solid.js projects will be easier to maintain, test, and scale over time.
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.
# 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.
# Component Design Standards for Solid.js This document outlines the component design standards for Solid.js, providing guidelines for creating reusable, maintainable, and performant components. It is intended to guide developers and inform AI coding assistants. ## 1. Component Architecture and Structure ### 1.1. Standard: Single Responsibility Principle **Do This:** Each component should have a clear, single purpose. Decompose complex functionalities into smaller, composable components. **Don't Do This:** Avoid creating "God Components" that handle multiple unrelated responsibilities. **Why:** Adhering to the Single Responsibility Principle (SRP) promotes modularity, testability, and reusability. Smaller, focused components are easier to understand, maintain, and reuse across different parts of the application. """jsx // Good: Separated component for displaying user info function UserInfo({ user }) { return ( <div> <h2>{user.name}</h2> <p>Email: {user.email}</p> </div> ); } // Good: Separated component for displaying user avatar function UserAvatar({ user }) { return ( <img src={user.avatarUrl} alt={user.name} /> ); } function UserProfile({ user }) { return ( <div> <UserAvatar user={user} /> <UserInfo user={user} /> </div> ); } // Bad: Component rendering avatar and info together, less reusable. function UserProfileCombined({user}) { return ( <div> <img src={user.avatarUrl} alt={user.name} /> <h2>{user.name}</h2> <p>Email: {user.email}</p> </div> ); } """ ### 1.2. Standard: Component Composition **Do This:** Favor composition over inheritance. Use props to configure components and pass children to create flexible UI structures. **Don't Do This:** Rely heavily on class inheritance or complex conditional rendering within a single component. **Why:** Composition promotes code reuse and avoids the tight coupling associated with inheritance. It makes components more adaptable to different contexts. """jsx // Good: Composing a Layout component with children function Layout({ children }) { return ( <div className="layout"> <header>Header</header> <main>{children}</main> <footer>Footer</footer> </div> ); } function HomePage() { return ( <Layout> <h1>Welcome to the Home Page</h1> <p>Some content here.</p> </Layout> ); } // Bad: Trying to handle layout within the page component. function HomePageCombined() { return ( <div className="layout"> <header>Header</header> <main> <h1>Welcome to the Home Page</h1> <p>Some content here.</p> </main> <footer>Footer</footer> </div> ); } """ ### 1.3. Standard: Named Exports **Do This:** Use named exports for components to improve discoverability and refactoring. **Don't Do This:** Rely solely on default exports, especially in larger projects. **Why:** Named exports make it easier to identify and import specific components, improving code readability and maintainability. They also facilitate tree shaking and reduce bundle size. """jsx // Good: Named export export function Button({ children, onClick }) { return <button onClick={onClick}>{children}</button>; } // Bad: Default export (less discoverable) export default function Button({ children, onClick }) { return <button onClick={onClick}>{children}</button>; } // Usage of named import import { Button } from './components/Button'; """ ## 2. Component Implementation Details ### 2.1. Standard: Prop Types and Validation **Do This:** Define prop types using TypeScript or PropTypes to ensure data integrity and provide helpful error messages during development. **Don't Do This:** Skip prop type definitions, especially for components that accept complex or critical data. **Why:** Prop type validation helps catch errors early, improves code reliability, and makes components easier to understand. TypeScript provides static type checking, while PropTypes offers runtime validation. """tsx // TypeScript Example interface ButtonProps { children: string; onClick: () => void; disabled?: boolean; // Optional prop } function Button(props: ButtonProps) { return ( <button onClick={props.onClick} disabled={props.disabled}> {props.children} </button> ); } // PropTypes Example (less common with TypeScript, but still valid) import PropTypes from 'prop-types'; function LegacyButton({ children, onClick }) { return <button onClick={onClick}>{children}</button>; } LegacyButton.propTypes = { children: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, }; // Usage: function App() { const handleClick = () => { alert("Button clicked!"); }; return ( <Button onClick={handleClick} children="Click Me" /> ); } """ ### 2.2. Standard: Controlled vs. Uncontrolled Components **Do This:** Choose between controlled and uncontrolled components based on your use case. Consider using controlled components when you need fine-grained control over input values and validation. Use uncontrolled components for simpler cases. **Don't Do This:** Mix controlled and uncontrolled patterns within the same component without a clear reason. **Why:** Controlled components provide a single source of truth for input values, making it easier to implement validation and complex logic. Uncontrolled components can simplify development for basic forms. """jsx // Good: Controlled component import { createSignal } from 'solid-js'; function ControlledInput() { const [inputValue, setInputValue] = createSignal(''); const handleChange = (event) => { setInputValue(event.target.value); }; return ( <div> <input type="text" value={inputValue()} onChange={handleChange} /> <p>Value: {inputValue()}</p> </div> ); } // Good: Uncontrolled component function UncontrolledInput() { const handleSubmit = (event) => { event.preventDefault(); const value = event.target.elements.myInput.value; alert("Uncontrolled input value: ${value}"); }; return ( <form onSubmit={handleSubmit}> <input type="text" name="myInput" /> <button type="submit">Submit</button> </form> ); } """ ### 2.3. Standard: Handling Side Effects **Do This:** Use effects ("createEffect", "onMount", "onCleanup") to manage side effects such as data fetching, DOM manipulation, and subscriptions. **Don't Do This:** Perform side effects directly within the component's render function or outside effects. **Why:** Effects allow you to isolate and manage side effects, preventing unexpected behavior and improving performance. They also provide mechanisms for cleanup, which is essential for avoiding memory leaks. """jsx // Good: Using createEffect for data fetching import { createSignal, createEffect, onMount } from 'solid-js'; function DataFetcher() { const [data, setData] = createSignal(null); const [loading, setLoading] = createSignal(true); const fetchData = async () => { try { const response = await fetch('https://api.example.com/data'); const result = await response.json(); setData(result); } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); } }; onMount(() => { fetchData(); }); return ( <div> {loading() ? <p>Loading...</p> : <pre>{JSON.stringify(data(), null, 2)}</pre>} </div> ); } // Using onCleanup to unsubscribe from an event import { createSignal, createEffect, onMount, onCleanup } from 'solid-js'; function EventListenerComponent() { const [count, setCount] = createSignal(0); onMount(() => { const increment = () => { setCount(prev => prev + 1); }; window.addEventListener('click', increment); onCleanup(() => { window.removeEventListener('click', increment); console.log('Event listener removed!'); }); }); return ( <div> <p>Count: {count()}</p> </div> ); } """ ### 2.4. Standard: Using Refs **Do This:** Employ refs ("createRef") to access DOM elements or component instances directly when necessary. **Don't Do This:** Overuse refs. Typically data should be managed with signals. Avoid direct DOM manipulation if the framework data binding can handle it. **Why:** Refs provide a way to interact with the underlying DOM or component instances, which can be useful for focus management, measuring elements, or integrating with third-party libraries. """jsx // Good: Using a ref for focus management import { createRef, onMount } from 'solid-js'; function FocusableInput() { const inputRef = createRef(); onMount(() => { inputRef.current.focus(); }); return <input type="text" ref={inputRef} />; } """ ### 2.5. Standard: Performance Optimization **Do This:** Utilize "memo" and "batch" to prevent unnecessary re-renders of components and efficiently update multiple signals. Understand memoization limitations and avoid over-optimization. **Don't Do This:** Neglect performance considerations, especially when dealing with large datasets or complex UI interactions. Wrap every component with "memo" without a good reason. **Why:** "memo" helps optimize performance by memoizing components and preventing re-renders when the props haven't changed. "batch" allows to change multiple signals in a single update, reducing the number of re-renders trigged. """jsx // Good: Using memo to prevent unnecessary re-renders import { createSignal, memo } from 'solid-js'; function DisplayValue({ value }) { console.log('DisplayValue rendered'); // Check when it renders return <p>Value: {value}</p>; } const MemoizedDisplayValue = memo(DisplayValue); function App() { const [count, setCount] = createSignal(0); return ( <div> <button onClick={() => setCount(count() + 1)}>Increment</button> <MemoizedDisplayValue value={count()} /> </div> ); } // Good: Using batch to update multiple signals. import { createSignal, batch } from 'solid-js'; function MultiUpdate() { const [firstName, setFirstName] = createSignal('John'); const [lastName, setLastName] = createSignal('Doe'); const updateName = () => { batch(() => { setFirstName('Jane'); setLastName('Smith'); }); console.log('Signals updated'); }; return ( <div> <p>First Name: {firstName()}</p> <p>Last Name: {lastName()}</p> <button onClick={updateName}>Update Name</button> </div> ); } """ ### 2.6. Standard: Context API **Do This:** Use the Context API ("createContext", "useContext") for sharing data between components without prop drilling. **Don't Do This:** Overuse Context API for all data management. It is better for app-wide configuration or shared state that many components need. Avoid using context for passing props down a single branch of components. **Why:** Context API simplifies data sharing in complex component trees, improving code readability and maintainability. """jsx // Good: Using Context API import { createContext, useContext, createSignal } from 'solid-js'; // Create a context const ThemeContext = createContext({ theme: 'light', setTheme: () => {} }); // Create a provider component function ThemeProvider({ children }) { const [theme, setTheme] = createSignal('light'); const value = { theme: theme(), setTheme: setTheme }; return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } // Create a consumer component function ThemedComponent() { const themeContext = useContext(ThemeContext); const toggleTheme = () => { themeContext.setTheme(themeContext.theme === 'light' ? 'dark' : 'light'); }; return ( <div style={{ background: themeContext.theme === 'light' ? '#fff' : '#333', color: themeContext.theme === 'light' ? '#000' : '#fff' }}> <p>Current theme: {themeContext.theme}</p> <button onClick={toggleTheme}>Toggle Theme</button> </div> ); } // Usage in App root: function App() { return ( <ThemeProvider> <ThemedComponent /> </ThemeProvider> ); } """ ### 2.7. Standard: Fragments **Do This:** Use Fragments ("<>...</>") to group multiple elements without adding an extra DOM node. **Don't Do This:** Wrap components in unnecessary "<div>" elements just to satisfy the requirement of returning a single root element. **Why:** Fragments prevent the creation of extra DOM nodes, which can improve performance and simplify the DOM structure. """jsx // Good: Using Fragments function MyComponent() { return ( <> <h1>Title</h1> <p>Content</p> </> ); } // Bad: Unnecessary div function MyComponentBad() { return ( <div> <h1>Title</h1> <p>Content</p> </div> ); } """ ## 3. Component Testing ### 3.1. Standard: Unit Testing **Do This:** Write unit tests for individual components to verify their functionality and ensure they behave as expected. Use a testing library like Jest or Vitest with Solid Testing Library. **Don't Do This:** Skip unit tests, especially for complex or critical components, as this increases the risk of regressions and makes it harder to refactor code. **Why:** Unit tests provide a safety net for your code, making it easier to identify and fix bugs early in the development process. They also serve as documentation for how components are intended to be used. """javascript // Example Unit Test - using Vitest & solid-testing-library import { render, screen } from 'solid-testing-library'; import { createSignal } from 'solid-js'; import { describe, it, expect } from 'vitest'; function Counter() { const [count, setCount] = createSignal(0); return ( <div> <p>Count: {count()}</p> <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); } describe('Counter Component', () => { it('should increment the count when the button is clicked', async () => { render(() => <Counter />); const incrementButton = screen.getByText('Increment'); expect(screen.getByText('Count: 0')).toBeInTheDocument(); await incrementButton.click(); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); }); """ ### 3.2. Standard: Integration Testing **Do This:** Write integration tests to verify that components work correctly together and that the application behaves as expected. **Don't Do This:** Rely solely on unit tests, as they may not catch issues that arise from interactions between components. **Why:** Integration tests ensure that different parts of the application work together correctly, reducing the risk of integration issues. ### 3.3. Standard: End-to-End Testing **Do This:** Write end-to-end (E2E) tests to verify the entire application flow, from user interaction to data persistence. Use a tool like Cypress or Playwright. **Don't Do This:** Neglect E2E tests, as they provide the most comprehensive coverage and ensure that the application works correctly in a real-world environment. **Why:** E2E tests simulate real user behavior, ensuring that the application works correctly from start to finish. They help catch issues that may not be apparent from unit or integration tests. ## 4. Styling Components ### 4.1. Standard: CSS Modules or Styled Components **Do This:** Use CSS Modules or Styled Components to scope styles to individual components, preventing naming collisions and improving code maintainability. **Don't Do This:** Use global CSS classes without a clear naming convention, as this can lead to style conflicts and make it harder to refactor code. **Why:** CSS Modules and Styled Components provide a way to encapsulate styles within components, preventing naming collisions and improving code organization. """jsx // Good: CSS Modules import styles from './MyComponent.module.css'; function MyComponent() { return ( <div className={styles.container}> <h1 className={styles.title}>Title</h1> <p className={styles.content}>Content</p> </div> ); } """ """jsx // Example: Styled Components import styled from 'styled-components'; const StyledContainer = styled.div" background-color: #f0f0f0; padding: 20px; border-radius: 5px; "; const StyledTitle = styled.h1" color: #333; font-size: 24px; "; function MyComponent() { return ( <StyledContainer> <StyledTitle>Hello, Styled Components!</StyledTitle> <p>This is a styled component example.</p> </StyledContainer> ); } """ ### 4.2. Standard: BEM Naming Convention (if using Standard CSS) **Do This:** If using standard CSS, follow the BEM (Block, Element, Modifier) naming convention to create clear and maintainable CSS classes. **Don't Do This:** Use ambiguous or inconsistent CSS class names, as this can make it harder to understand and maintain the styles. **Why:** BEM provides a clear and consistent naming convention for CSS classes, improving code readability and maintainability. """css /* Good: BEM Naming */ .block {} .block__element {} .block__element--modifier {} /* Bad: Ambiguous Naming */ .title {} .content {} """ ## 5. Accessibility ### 5.1. Standard: ARIA Attributes **Do This:** Use ARIA attributes to provide semantic information to assistive technologies, making the application more accessible to users with disabilities. **Don't Do This:** Neglect accessibility considerations, as this can exclude users with disabilities from using the application. **Why:** ARIA attributes provide a way to enhance the accessibility of web applications, making them more usable for people with disabilities. """jsx // Good: Using ARIA attributes function AccessibleButton({ onClick, children }) { return ( <button onClick={onClick} aria-label="Click me"> {children} </button> ); } """ ### 5.2. Standard: Semantic HTML **Do This:** Use semantic HTML elements to structure the application, providing meaning to the content and improving accessibility. **Don't Do This:** Use generic "<div>" or "<span>" elements for all content, as this can make the application harder to understand and navigate. **Why:** Semantic HTML elements provide meaning to the content, making it easier for assistive technologies and search engines to understand the structure of the application. """html <!-- Good: Semantic HTML --> <nav> <ul> <li><a href="#">Home</a></li> <li><a href="#">About</a></li> </ul> </nav> <article> <h1>Title</h1> <p>Content</p> </article> """ ## 6. Solid.js Specific Best Practices These are component design considerations that leverage Solid.js' strengths: ### 6.1. Standard: Granular Updates with Signals **Do This:** Utilize the fine-grained reactivity of Solid.js by ensuring signals are the only sources of state within components. This enables precise DOM updates. **Don't Do This:** Mutate data structures directly, or use approaches that force broad component re-renders, negating Solid's performance advantages. **Why:** Solid.js is incredibly efficient because it only updates the specific parts of the DOM that have changed. By relying on signals, you tap into this reactivity system optimally. """jsx import { createSignal } from "solid-js"; function MyComponent() { const [name, setName] = createSignal("Initial Name"); return ( <div> <p>Name: {name()}</p> <button onClick={() => setName("New Name")}>Change Name</button> </div> ); } """ ### 6.2. Standard: Avoid Unnecessary Abstraction **Do This:** Keep components simple and focused, avoiding over-engineering abstractions too early. Solid.js excels at direct DOM manipulation and optimized updates. **Don't Do This:** Force complex compositional patterns if they don't translate to benefits in performance or maintainability. Simplicity can be a virtue in Solid.js. **Why:** Solid.js already handles many performance concerns under the hood. Overly complex component structures can sometimes hinder, rather than help. ### 6.3 Standard: Use Solid's Control Flow Components for Rendering Lists and Conditionals **Do This:** Use Solid's "<Show>", "<For>", and "<Switch>" components rather than relying on .map() and ternary operators for conditional rendering, as they hook into the reactivity system more efficiently. **Don't Do This:** Fall back upon array methods and ternary operators for conditionals unless you're sure there will be few state changes and updates to these areas. **Why:** Solid's control flow components optimize rendering and re-rendering compared to more generic JavaScript constructs """jsx import { createSignal, For, Show } from "solid-js"; function ListComponent() { const [items, setItems] = createSignal(["Item 1", "Item 2", "Item 3"]); const [showList, setShowList] = createSignal(true); return ( <> <Show when={showList()}> <ul> <For each={items()}>{(item) => <li>{item}</li>}</For> </ul> </Show> <button onClick={() => setShowList(!showList())}>Toggle List</button> </> ); } """ These standards aim to provide a comprehensive guide to building well-structured, maintainable, and performant Solid.js components, taking advantage of the framework's unique reactive model and emphasizing best practices for modern web development.
# 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.
# Testing Methodologies Standards for Solid.js This document outlines testing methodologies standards for Solid.js applications. Adhering to these standards will improve code quality, maintainability, and reliability by ensuring thorough testing coverage and promoting consistent testing practices across the codebase. ## 1. General Testing Principles ### 1.1. Adopt a Test-Driven Development (TDD) or Behavior-Driven Development (BDD) Approach * **Do This:** Write tests before implementing the actual code. This helps clarify requirements and ensures that the code is testable from the beginning. * **Don't Do This:** Write tests as an afterthought. This can lead to incomplete test coverage and code that is difficult to test. **Why:** TDD and BDD foster a culture of quality and provide a clear specification for each component. They greatly reduce debugging time and ensure that the application behaves as expected from the outset. """javascript // TDD Example (using Jest) // src/components/Counter.jsx // (Initially empty, or with minimal scaffolding) // test/components/Counter.test.jsx describe("Counter Component", () => { it("should initialize with a count of 0", () => { // Assertion about initial state BEFORE writing the Counter component }); it("should increment the count when the button is clicked", () => { // Assertion about incrementing count BEFORE writing the Counter component }); }); """ ### 1.2. Aim for High Test Coverage * **Do This:** Strive for test coverage of at least 80% for unit, integration, and end-to-end tests, focusing on crucial business logic and complex components. * **Don't Do This:** Neglect testing less critical components or leave complex logic untested. This can create vulnerabilities and lead to unexpected bugs. **Why:** High test coverage reduces the risk of introducing bugs and provides confidence in the code's correctness. It also serves as living documentation, demonstrating how each part of the application is intended to function. ### 1.3. Write Isolated and Independent Tests * **Do This:** Each test case should be independent of other tests. Use mocking and stubbing to isolate the component under test and avoid dependencies on external services or databases. * **Don't Do This:** Allow tests to share state or rely on the execution order. This can lead to flaky tests and unpredictable results. **Why:** Isolated tests are easier to debug and maintain. They also allow for parallel test execution, which can significantly reduce the overall testing time. ### 1.4. Follow the AAA Pattern * **Do This:** Structure each test case using the Arrange-Act-Assert (AAA) pattern. Arrange the necessary preconditions, Act on the component under test, and Assert that the expected results are achieved. * **Don't Do This:** Mix the arrangement, action, and assertion steps, making the test case difficult to understand and maintain. **Why:** The AAA pattern improves the readability and organization of test cases. It makes it clear what is being tested and what the expected outcome is. """javascript // AAA Pattern Example it("should increment the count when the button is clicked", () => { // Arrange const { getByText } = render(() => <Counter />); const incrementButton = getByText("Increment"); // Act fireEvent.click(incrementButton); // Assert expect(getByText("Count: 1")).toBeInTheDocument(); }); """ ## 2. Unit Testing ### 2.1. Focus on Component Logic and Functionality * **Do This:** Unit tests should focus on testing the logic and functionality of individual components or functions in isolation. * **Don't Do This:** Attempt to test the entire application flow in a single unit test. This makes the test fragile and difficult to maintain. **Why:** Unit tests provide granular feedback on the correctness of individual components. They quickly identify bugs in the code and make it easier to refactor the application. ### 2.2. Use a Dedicated Testing Library * **Do This:** Choose a suitable testing library such as Jest, Vitest, or Mocha with Chai or Assert. * **Don't Do This:** Rely on console.log statements or manual testing to verify the correctness of the code. This is not scalable or reliable. **Why:** Testing libraries provide a rich set of assertions, mocking capabilities, and test runners. They enable you to write comprehensive and automated unit tests. Vitest is especially strong for Solid.js apps because it uses Vite, minimizing configuration needs for Solid projects. """javascript // Vitest Unit Test Example (Counter Component) import { render, fireEvent, screen } from '@solidjs/testing-library'; import { describe, it, expect, vi } from 'vitest'; import Counter from '../src/components/Counter'; import { createSignal } from 'solid-js'; describe('Counter Component', () => { it('should initialize with a count of 0', () => { render(() => <Counter />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('should increment the count when the button is clicked', async () => { render(() => <Counter />); const incrementButton = screen.getByText('Increment'); await fireEvent.click(incrementButton); // fireEvent is async expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); it('should call the onIncrement callback when the button is clicked', async () => { const onIncrement = vi.fn(); // Spy function render(() => <Counter onIncrement={onIncrement} />); const incrementButton = screen.getByText('Increment'); await fireEvent.click(incrementButton); // fireEvent is async expect(onIncrement).toHaveBeenCalledTimes(1); }); }); """ ### 2.3. Mock External Dependencies * **Do This:** Use mocking libraries like "vitest.fn()", Jest's "jest.fn()", or "sinon" to mock external dependencies such as API calls or third-party libraries. * **Don't Do This:** Directly call external services or databases in unit tests. This can lead to slow and unreliable tests. **Why:** Mocking allows you to isolate the component under test and control its behavior. It also makes it possible to test edge cases and error scenarios that would be difficult to reproduce with real dependencies. The updated Vitest API shown in the example below is preferable to earlier methods """javascript // Mocking Example (using Vitest's vi) import { render, fireEvent, screen } from '@solidjs/testing-library'; import { describe, it, expect, vi } from 'vitest'; import MyComponent from '../src/components/MyComponent'; import { fetchData } from '../src/api'; // Assume this function fetches external data vi.mock('../src/api', () => ({ // Correct way to mock named exports fetchData: vi.fn().mockResolvedValue({ data: 'mocked data' }), })); describe('MyComponent', () => { it('should display fetched data', async () => { render(() => <MyComponent />); expect(await screen.findByText('mocked data')).toBeInTheDocument(); // Correctly use await }); }); """ Be mindful of asynchronous operations within Solid components especially those leveraging "createResource" """javascript // test/components/AsyncComponent.test.jsx import { render, screen, waitFor } from '@solidjs/testing-library'; import { describe, it, expect } from 'vitest'; import AsyncComponent from '../src/components/AsyncComponent'; describe('AsyncComponent', () => { it('should display loading state initially', () => { render(() => <AsyncComponent />); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('should display data after loading', async () => { render(() => <AsyncComponent />); // Waits for the 'Not Loading' state to appear before checking for the data await waitFor(() => expect(screen.getByText('Not Loading')).toBeInTheDocument())// waits for loading state to disappear (or another state to appear) expect(screen.getByText('Data loaded successfully!')).toBeInTheDocument(); }); it('should handle error state', async () => { const originalFetch = global.fetch; global.fetch = () => Promise.reject(new Error('Failed to fetch')); render(() => <AsyncComponent />); await waitFor(() => expect(screen.getByText('Error')).toBeInTheDocument()); // explicitly waits for error message global.fetch = originalFetch; // Restore fetch }); }); // src/components/AsyncComponent.jsx import { createResource, createSignal } from 'solid-js'; const fetchData = async () => { return new Promise(resolve => { setTimeout(() => { resolve('Data loaded successfully!'); }, 500); }); }; function AsyncComponent() { const [loading, setLoading] = createSignal(true); const [data, { mutate, refetch }] = createResource(fetchData); // Side effect to update loading state when data is resolved or rejected data.state === "pending" ? setLoading(true) : setLoading(false); if (data.error) { return <div>Error</div>; } return ( <> <div>{loading() ? 'Loading...' : 'Not Loading'}</div> {data() && <div>{data()}</div>} </> ); } export default AsyncComponent; """ ### 2.4. Test Component Interactions and Props * **Do This:** Verify that components correctly handle different prop values, events, and signals. Use event firing utilities, such as "@solidjs/testing-library"'s "fireEvent" or "userEvent", to simulate user interactions. * **Don't Do This:** Only test the default behavior of a component. This leaves edge cases and unusual prop combinations untested. **Why:** Components often have complex interaction logic. Testing these interactions ensures that the component behaves correctly in all possible scenarios. """javascript // Testing Prop Changes and Event Handling import { render, fireEvent, screen } from '@solidjs/testing-library'; import { describe, it, expect, afterEach } from 'vitest'; import MyButton from '../src/components/MyButton'; describe('MyButton Component', () => { it('should display the correct label based on props', () => { render(() => <MyButton label="Click Me" />); expect(screen.getByText('Click Me')).toBeInTheDocument(); render(() => <MyButton label="Submit" />); expect(screen.getByText('Submit')).toBeInTheDocument(); }); it('should call the onClick handler when clicked', async () => { const onClick = vi.fn(); render(() => <MyButton label="Test Button" onClick={onClick} />); const button = screen.getByText('Test Button'); await fireEvent.click(button); expect(onClick).toHaveBeenCalledTimes(1); }); it('should not call onClick if disabled prop is true', async () => { const onClick = vi.fn(); render(() => <MyButton label="Test Button" onClick={onClick} disabled={true} />); const button = screen.getByText('Test Button'); await fireEvent.click(button); expect(onClick).not.toHaveBeenCalled(); expect(button.closest('button')).toBeDisabled() // Correctly queries the disabled property }); }); """ ### 2.5. Test Reactive Updates * **Do This:** Solid's reactivity requires careful consideration when testing. Use "@solidjs/testing-library"'s "render" which automatically wraps your component for reactivity. Verify that components correctly update the DOM in response to signal changes. Utilize tools like "createSignal" and "createEffect" in tests where reactive behavior needs close inspection. * **Don't Do This:** Assume that changes to signals will automatically update the rendered output without explicit checks or waiting. **Why:** Solid's fine-grained reactivity model demands explicit tests to confirm DOM updates are triggered correctly. """javascript import { render, screen } from '@solidjs/testing-library'; import { createSignal } from 'solid-js'; import { describe, it, expect } from 'vitest'; function ReactiveComponent() { const [count, setCount] = createSignal(0); return ( <div> <p>Count: {count()}</p> <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); } describe('ReactiveComponent', () => { it('should update the count when the button is clicked', async () => { const { getByText } = render(() => <ReactiveComponent/>); // Use render from @solidjs/testing-library const incrementButton = getByText('Increment'); const countDisplay = getByText(/Count: 0/); // Match the initial count value. incrementButton.click(); // Use await screen.findByText to wait for the update to be visible expect(await screen.findByText(/Count: 1/)).toBeVisible() }); }); """ ## 3. Integration Testing ### 3.1. Verify Interactions Between Components * **Do This:** Integration tests should verify that different components work together correctly. This includes testing data flow between components, event handling, and navigation. * **Don't Do This:** Test individual component logic in integration tests. That's the role of unit tests. **Why:** Integration tests provide confidence that the different parts of the application are properly connected and interact as expected. ### 3.2. Use a Realistic Testing Environment * **Do This:** Configure a testing environment that closely resembles the production environment. This includes setting up a mock API server, configuring routing, and using the same browser versions. * **Don't Do This:** Use simplified or unrealistic testing environments. This can mask integration issues that would only appear in production. **Why:** Realistic testing environments increase the reliability of integration tests and reduce the risk of introducing bugs in production. ### 3.3. Test Data Flow and State Management * **Do This:** Verify that data flows correctly between components and that the state management system is working as expected. This includes testing Redux, Zustand, or other state management libraries. * **Don't Do This:** Ignore testing the data flow and state management. This can lead to data inconsistencies and unexpected behavior. **Why:** Proper state management is essential for building complex applications. Integration tests ensure that the state is correctly updated and that components react appropriately to state changes. """javascript // Integration Test Example (using Vitest and @solidjs/testing-library) import { render, fireEvent, screen } from '@solidjs/testing-library'; import { describe, it, expect } from 'vitest'; import { createSignal } from 'solid-js'; //Mocked Components function ChildComponent(props) { return <p>Value from parent: {props.value()}</p>; } function ParentComponent() { const [value, setValue] = createSignal("initial value"); return ( <div> <p>Parent value: {value()}</p> <button onClick={() => setValue("updated value")}>Update Value</button> <ChildComponent value={value} /> </div> ); } describe('ParentComponent with ChildComponent Integration', () => { it('should pass and update the state from ParentComponent to ChildComponent', async () => { render(() => <ParentComponent />); const parentValueElement = screen.getByText(/Parent value:/); const childValueElement = screen.getByText(/Value from parent:/); expect(parentValueElement).toHaveTextContent('Parent value: initial value'); expect(childValueElement).toHaveTextContent('Value from parent: initial value'); const updateButton = screen.getByText('Update Value'); await fireEvent.click(updateButton); expect(parentValueElement).toHaveTextContent('Parent value: updated value'); expect(childValueElement).toHaveTextContent('Value from parent: updated value'); }); }); """ ## 4. End-to-End (E2E) Testing ### 4.1. Simulate Real User Interactions * **Do This:** E2E tests should simulate real user interactions with the application. This includes navigating through the application, filling out forms, and clicking buttons. * **Don't Do This:** Test internal implementation details or bypass the user interface. This makes the tests fragile and less relevant to the user experience. **Why:** E2E tests provide the highest level of confidence that the application is working correctly from the user's perspective. They also help to identify integration issues that may not be caught by unit or integration tests. ### 4.2. Use a Browser Automation Tool * **Do This:** Choose a browser automation tool such as Playwright or Cypress. configure automated browser tests that run from the user side. * **Don't Do This:** Manually test the application or rely on manual testing to verify the end-to-end functionality. This is not scalable or reliable. **Why:** Browser automation tools allow you to write automated E2E tests that can be run repeatedly. They also provide powerful debugging tools that make it easier to identify and fix issues. ### 4.3. Test Across Multiple Browsers and Devices * **Do This:** Run E2E tests across multiple browsers (Chrome, Firefox, Safari) and devices (desktops, tablets, mobile phones) to ensure that the application is compatible with different environments. * **Don't Do This:** Only test on a single browser or device. This can mask compatibility issues that would affect users in other environments. **Why:** Cross-browser and cross-device testing ensures that the application provides a consistent user experience across all platforms. ### 4.4. Test User Flows and Critical Paths * **Do This:** Focus E2E tests on testing user flows, such as registration, login, checkout, and data input. * **Don't Do This:** Test every single feature or interaction in the application. This can make the tests too complex and time-consuming to maintain. **Why:** E2E tests should prioritize testing critical paths, such as user flows, to confirm reliability in essential functions. **Example using Playwright (with adaptations for Solid's reactivity)** """javascript // e2e/example.spec.js (Playwright example) import { test, expect } from '@playwright/test'; test('Counter increments correctly', async ({ page }) => { await page.goto('http://localhost:3000'); // Replace with your dev server URL const countElement = await page.locator('#counter-value'); const incrementButton = await page.locator('#increment-button'); expect(await countElement.textContent()).toBe('0'); //Initial Value await incrementButton.click(); await page.waitForTimeout(100); // Necessary due to Solid possibly needing a tick to render expect(await countElement.textContent()).toBe('1'); // Post Increment value; }); """ In this example, you may need to adjust your timing to account for Solid's reactivity. Adding simple timeouts such as "page.waitForTimeout" may be necessary to stabilize the test. ## 5. Testing Asynchronous Operations ### 5.1. Handle Asynchronous Updates in Solid * **Do This:** When working with Solid.js, remember that many UI updates are asynchronous due to the framework's reactive nature. Use "await" with "@solidjs/testing-library's" "findBy*" queries, or "waitFor" utilities, and ensure that you're correctly accounting for time when making assertions about the UI. * **Don't Do This:** Make synchronous assertions immediately after triggering an event that causes an asynchronous update. This can lead to flaky tests that pass occasionally but fail other times without any code changes. **Why:** Solid's fine-grained reactivity executes updates efficiently, but this means UI changes aren't always instantaneous. Waiting for promises and using correct query selectors helps produce reliable tests. ### 5.2. Await Promises * **Do This:** If you are testing any async operation, even indirectly fired from a component like button click event. Use "await" keyword. * **Don't Do This:** Forget to await promises or async function inside tests. **Why:** Async functions can't complete execution until its promise is resolved and synchronous operations are done. Awaiting will make your tests less flaky and avoid unexpected behavior. ## 6. Accessibility Testing ### 6.1. Use Accessibility Testing Tools * **Do This:** Integrate accessibility testing tools like axe-core with your test suite to automatically check for common accessibility issues. * **Don't Do This:** Neglect accessibility testing. This can exclude users with disabilities and harm the application's overall usability. **Why:** Accessibility testing ensures that the application is usable by people with disabilities. It is also a legal requirement in many jurisdictions and demonstrates a commitment to inclusivity. ### 6.2. Test with Screen Readers * **Do This:** Manually test the application with screen readers such as NVDA or VoiceOver to verify that the content is properly announced and that the application is navigable using assistive technologies. * **Don't Do This:** Rely solely on automated accessibility testing tools. This can miss issues that only become apparent when using a screen reader. **Why:** Screen reader tests are essential for verifying the accuracy and usability of accessibility features. ## 7. Code Review and Continuous Integration ### 7.1. Enforce Testing Standards in Code Reviews * **Do This:** Include testing as part of the code review process. Verify that new code is adequately tested and that the tests are following the standards outlined in this document. * **Don't Do This:** Allow untested or poorly tested code to be merged into the codebase. This can erode code quality and increase the risk of introducing bugs. **Why:** Code reviews are a critical opportunity to enforce testing standards and improve the overall quality of the codebase. ### 7.2. Integrate Tests into Continuous Integration (CI) Pipelines * **Do This:** Integrate unit, integration, and E2E tests into the CI pipelines. Configure the CI system to automatically run the tests whenever code is pushed to the repository. * **Don't Do This:** Skip running tests in the CI pipeline. This defeats the purpose of having automated tests and increases the risk of introducing bugs in production. **Why:** CI pipelines ensure that tests are run consistently and automatically. They provide immediate feedback on the correctness of new code and prevent bugs from being merged into the main branch.