# SolidJS Patterns & Best Practices Common patterns, recipes, and best practices for SolidJS development. ## Component Patterns ### Controlled vs Uncontrolled Inputs **Controlled:** ```tsx function ControlledInput() { const [value, setValue] = createSignal(""); return ( setValue(e.currentTarget.value)} /> ); } ``` **Uncontrolled with ref:** ```tsx function UncontrolledInput() { let inputRef: HTMLInputElement; const handleSubmit = () => { console.log(inputRef.value); }; return ( <> ); } ``` ### Compound Components ```tsx const Tabs = { Root: (props: ParentProps<{ defaultTab?: string }>) => { const [activeTab, setActiveTab] = createSignal(props.defaultTab ?? ""); return (
{props.children}
); }, List: (props: ParentProps) => (
{props.children}
), Tab: (props: ParentProps<{ value: string }>) => { const ctx = useTabsContext(); return ( ); }, Panel: (props: ParentProps<{ value: string }>) => { const ctx = useTabsContext(); return (
{props.children}
); } }; // Usage First Second First Content Second Content ``` ### Render Props ```tsx function MouseTracker(props: { children: (pos: { x: number; y: number }) => JSX.Element; }) { const [pos, setPos] = createSignal({ x: 0, y: 0 }); onMount(() => { const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener("mousemove", handler); onCleanup(() => window.removeEventListener("mousemove", handler)); }); return <>{props.children(pos())}; } // Usage {(pos) =>
Mouse: {pos.x}, {pos.y}
}
``` ### Higher-Order Components ```tsx function withAuth

(Component: Component

) { return (props: P) => { const { user } = useAuth(); return ( }> ); }; } const ProtectedDashboard = withAuth(Dashboard); ``` ### Polymorphic Components ```tsx type PolymorphicProps = { as?: E; } & JSX.IntrinsicElements[E]; function Box( props: PolymorphicProps ) { const [local, others] = splitProps(props as PolymorphicProps<"div">, ["as"]); return ; } // Usage Default div Section element Button ``` ## State Patterns ### Derived State with Multiple Sources ```tsx function SearchResults() { const [query, setQuery] = createSignal(""); const [filters, setFilters] = createSignal({ category: "all" }); const results = createMemo(() => { const q = query().toLowerCase(); const f = filters(); return allItems() .filter(item => item.name.toLowerCase().includes(q)) .filter(item => f.category === "all" || item.category === f.category); }); return {item => }; } ``` ### State Machine Pattern ```tsx type State = "idle" | "loading" | "success" | "error"; type Event = { type: "FETCH" } | { type: "SUCCESS"; data: any } | { type: "ERROR"; error: Error }; function createMachine(initial: State) { const [state, setState] = createSignal(initial); const [data, setData] = createSignal(null); const [error, setError] = createSignal(null); const send = (event: Event) => { const current = state(); switch (current) { case "idle": if (event.type === "FETCH") setState("loading"); break; case "loading": if (event.type === "SUCCESS") { setData(event.data); setState("success"); } else if (event.type === "ERROR") { setError(event.error); setState("error"); } break; } }; return { state, data, error, send }; } ``` ### Optimistic Updates ```tsx const [todos, setTodos] = createStore([]); async function deleteTodo(id: string) { const original = [...unwrap(todos)]; // Optimistic remove setTodos(todos => todos.filter(t => t.id !== id)); try { await api.deleteTodo(id); } catch { // Rollback on error setTodos(reconcile(original)); } } ``` ### Undo/Redo ```tsx function createHistory(initial: T) { const [past, setPast] = createSignal([]); const [present, setPresent] = createSignal(initial); const [future, setFuture] = createSignal([]); const canUndo = () => past().length > 0; const canRedo = () => future().length > 0; const set = (value: T | ((prev: T) => T)) => { const newValue = typeof value === "function" ? (value as (prev: T) => T)(present()) : value; setPast(p => [...p, present()]); setPresent(newValue); setFuture([]); }; const undo = () => { if (!canUndo()) return; const previous = past()[past().length - 1]; setPast(p => p.slice(0, -1)); setFuture(f => [present(), ...f]); setPresent(previous); }; const redo = () => { if (!canRedo()) return; const next = future()[0]; setPast(p => [...p, present()]); setFuture(f => f.slice(1)); setPresent(next); }; return { value: present, set, undo, redo, canUndo, canRedo }; } ``` ## Custom Hooks/Primitives ### useLocalStorage ```tsx function createLocalStorage(key: string, initialValue: T) { const stored = localStorage.getItem(key); const initial = stored ? JSON.parse(stored) : initialValue; const [value, setValue] = createSignal(initial); createEffect(() => { localStorage.setItem(key, JSON.stringify(value())); }); return [value, setValue] as const; } ``` ### useDebounce ```tsx function createDebounce(source: () => T, delay: number) { const [debounced, setDebounced] = createSignal(source()); createEffect(() => { const value = source(); const timer = setTimeout(() => setDebounced(() => value), delay); onCleanup(() => clearTimeout(timer)); }); return debounced; } // Usage const debouncedQuery = createDebounce(query, 300); ``` ### useThrottle ```tsx function createThrottle(source: () => T, delay: number) { const [throttled, setThrottled] = createSignal(source()); let lastRun = 0; createEffect(() => { const value = source(); const now = Date.now(); if (now - lastRun >= delay) { lastRun = now; setThrottled(() => value); } else { const timer = setTimeout(() => { lastRun = Date.now(); setThrottled(() => value); }, delay - (now - lastRun)); onCleanup(() => clearTimeout(timer)); } }); return throttled; } ``` ### useMediaQuery ```tsx function createMediaQuery(query: string) { const mql = window.matchMedia(query); const [matches, setMatches] = createSignal(mql.matches); onMount(() => { const handler = (e: MediaQueryListEvent) => setMatches(e.matches); mql.addEventListener("change", handler); onCleanup(() => mql.removeEventListener("change", handler)); }); return matches; } // Usage const isMobile = createMediaQuery("(max-width: 768px)"); ``` ### useClickOutside ```tsx function createClickOutside( ref: () => HTMLElement | undefined, callback: () => void ) { onMount(() => { const handler = (e: MouseEvent) => { const el = ref(); if (el && !el.contains(e.target as Node)) { callback(); } }; document.addEventListener("click", handler); onCleanup(() => document.removeEventListener("click", handler)); }); } // Usage let dropdownRef: HTMLDivElement; createClickOutside(() => dropdownRef, () => setOpen(false)); ``` ### useIntersectionObserver ```tsx function createIntersectionObserver( ref: () => HTMLElement | undefined, options?: IntersectionObserverInit ) { const [isIntersecting, setIsIntersecting] = createSignal(false); onMount(() => { const el = ref(); if (!el) return; const observer = new IntersectionObserver(([entry]) => { setIsIntersecting(entry.isIntersecting); }, options); observer.observe(el); onCleanup(() => observer.disconnect()); }); return isIntersecting; } ``` ## Form Patterns ### Form Validation ```tsx function createForm>(initial: T) { const [values, setValues] = createStore(initial); const [errors, setErrors] = createStore>>({}); const [touched, setTouched] = createStore>>({}); const handleChange = (field: keyof T) => (e: Event) => { const target = e.target as HTMLInputElement; setValues(field as any, target.value as any); }; const handleBlur = (field: keyof T) => () => { setTouched(field as any, true); }; const validate = (validators: Partial string | undefined>>) => { let isValid = true; for (const [field, validator] of Object.entries(validators)) { if (validator) { const error = validator(values[field as keyof T]); setErrors(field as any, error as any); if (error) isValid = false; } } return isValid; }; return { values, errors, touched, handleChange, handleBlur, validate, setValues }; } // Usage const form = createForm({ email: "", password: "" }); {form.errors.email} ``` ### Field Array ```tsx function createFieldArray(initial: T[] = []) { const [fields, setFields] = createStore(initial); const append = (value: T) => setFields(f => [...f, value]); const remove = (index: number) => setFields(f => f.filter((_, i) => i !== index)); const update = (index: number, value: Partial) => setFields(index, v => ({ ...v, ...value })); const move = (from: number, to: number) => { setFields(produce(f => { const [item] = f.splice(from, 1); f.splice(to, 0, item); })); }; return { fields, append, remove, update, move }; } ``` ## Performance Patterns ### Virtualized List ```tsx function VirtualList(props: { items: T[]; itemHeight: number; height: number; renderItem: (item: T, index: number) => JSX.Element; }) { const [scrollTop, setScrollTop] = createSignal(0); const startIndex = createMemo(() => Math.floor(scrollTop() / props.itemHeight) ); const visibleCount = createMemo(() => Math.ceil(props.height / props.itemHeight) + 1 ); const visibleItems = createMemo(() => props.items.slice(startIndex(), startIndex() + visibleCount()) ); return (

setScrollTop(e.currentTarget.scrollTop)} >
{(item, i) => (
{props.renderItem(item, startIndex() + i())}
)}
); } ``` ### Lazy Loading with Intersection Observer ```tsx function LazyLoad(props: ParentProps<{ placeholder?: JSX.Element }>) { let ref: HTMLDivElement; const [isVisible, setIsVisible] = createSignal(false); onMount(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { rootMargin: "100px" } ); observer.observe(ref); onCleanup(() => observer.disconnect()); }); return (
{props.children}
); } ``` ### Memoized Component ```tsx // For expensive components that shouldn't re-render on parent updates function MemoizedExpensiveList(props: { items: Item[] }) { // Component only re-renders when items actually change return ( {(item) => } ); } ``` ## Testing Patterns ### Component Testing ```tsx import { render, fireEvent, screen } from "@solidjs/testing-library"; test("Counter increments", async () => { render(() => ); const button = screen.getByRole("button", { name: /increment/i }); expect(screen.getByText("Count: 0")).toBeInTheDocument(); fireEvent.click(button); expect(screen.getByText("Count: 1")).toBeInTheDocument(); }); ``` ### Testing with Context ```tsx function renderWithContext(component: () => JSX.Element) { return render(() => ( {component()} )); } test("Dashboard shows user", () => { renderWithContext(() => ); // ... }); ``` ### Testing Async Components ```tsx import { render, waitFor, screen } from "@solidjs/testing-library"; test("Loads user data", async () => { render(() => ); expect(screen.getByText(/loading/i)).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText("John Doe")).toBeInTheDocument(); }); }); ``` ## Error Handling Patterns ### Global Error Handler ```tsx function App() { return ( ( )} > }> {/* Routes */} ); } ``` ### Async Error Handling ```tsx function DataComponent() { const [data] = createResource(fetchData); return ( refetch()} /> {(data) => } ); } ``` ## Accessibility Patterns ### Focus Management ```tsx function Modal(props: ParentProps<{ isOpen: boolean; onClose: () => void }>) { let dialogRef: HTMLDivElement; let previousFocus: HTMLElement | null; createEffect(() => { if (props.isOpen) { previousFocus = document.activeElement as HTMLElement; dialogRef.focus(); } else if (previousFocus) { previousFocus.focus(); } }); return (
e.key === "Escape" && props.onClose()} > {props.children}
); } ``` ### Live Regions ```tsx function Notifications() { const [message, setMessage] = createSignal(""); return (
{message()}
); } ```