Every anti-pattern with WRONG (React) and CORRECT (SolidJS) code side by side.
function UserCard({ name, email, avatar }: UserCardProps) {
return (
<div class="card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
// Result: name, email, avatar are frozen at their initial values. Parent
// updates to these props are NEVER reflected in the rendered output.function UserCard(props: UserCardProps) {
const { name, email, avatar } = props;
return (
<div class="card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
// Same problem: values captured once, reactive connection severed.function UserCard(props: UserCardProps) {
const name = props.name; // Snapshot at component setup
return <h2>{name}</h2>; // Never updates
}function UserCard(props: UserCardProps) {
return (
<div class="card">
<img src={props.avatar} alt={props.name} />
<h2>{props.name}</h2>
<p>{props.email}</p>
</div>
);
}import { splitProps } from "solid-js";
function UserCard(props: UserCardProps & { class?: string }) {
const [local, rest] = splitProps(props, ["name", "email", "avatar"]);
return (
<div {...rest}>
<img src={local.avatar} alt={local.name} />
<h2>{local.name}</h2>
<p>{local.email}</p>
</div>
);
}function UserCard(props: UserCardProps) {
const displayName = () => props.name.toUpperCase();
return <h2>{displayName()}</h2>;
}function Counter() {
const [count, setCount] = createSignal(0);
const value = count(); // Frozen at 0
return (
<div>
<p>Count: {value}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}
// Clicking the button updates the signal, but {value} always shows 0.function DelayedLogger() {
const [message, setMessage] = createSignal("hello");
const msg = message(); // Captured once
setTimeout(() => {
console.log(msg); // Always "hello", even if signal changed
}, 2000);
return <input onInput={(e) => setMessage(e.target.value)} />;
}function Counter() {
const [count, setCount] = createSignal(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}function DelayedLogger() {
const [message, setMessage] = createSignal("hello");
setTimeout(() => {
console.log(message()); // Reads current value at execution time
}, 2000);
return <input onInput={(e) => setMessage(e.target.value)} />;
}import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <div>{count}</div>; // count is a value, not a function
}import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return <div>{count()}</div>; // count is a GETTER -- must call it
}import { useEffect } from "react";
function TitleUpdater() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // Manual dependency list
}import { createEffect, createSignal } from "solid-js";
function TitleUpdater() {
const [count, setCount] = createSignal(0);
// WRONG: [count] is passed as the initial prev value, NOT as deps
createEffect(() => {
document.title = `Count: ${count()}`;
}, [count]);
}import { createEffect, createSignal } from "solid-js";
function TitleUpdater() {
const [count, setCount] = createSignal(0);
createEffect(() => {
document.title = `Count: ${count()}`; // Auto-tracked
});
}import { useMemo } from "react";
function ExpensiveList({ items, filter }) {
const filtered = useMemo(
() => items.filter((i) => i.name.includes(filter)),
[items, filter]
);
return <ul>{filtered.map((i) => <li>{i.name}</li>)}</ul>;
}import { createMemo } from "solid-js";
import { For } from "solid-js";
function ExpensiveList(props: { items: Item[]; filter: string }) {
const filtered = createMemo(() =>
props.items.filter((i) => i.name.includes(props.filter))
);
return (
<ul>
<For each={filtered()}>
{(item) => <li>{item.name}</li>}
</For>
</ul>
);
}function PriceDisplay() {
const [price, setPrice] = createSignal(100);
const [tax, setTax] = createSignal(0.21);
const total = price() * (1 + tax()); // Computed ONCE at setup
return <p>Total: {total}</p>; // NEVER updates
}function DebugComponent() {
const [count, setCount] = createSignal(0);
console.log("Current count:", count()); // Logs ONCE at setup
return <button onClick={() => setCount((c) => c + 1)}>Click</button>;
}function PriceDisplay() {
const [price, setPrice] = createSignal(100);
const [tax, setTax] = createSignal(0.21);
const total = () => price() * (1 + tax()); // Function -- re-evaluates on access
return <p>Total: {total()}</p>; // Reactive
}function PriceDisplay() {
const [price, setPrice] = createSignal(100);
const [tax, setTax] = createSignal(0.21);
const total = createMemo(() => price() * (1 + tax())); // Cached, reactive
return <p>Total: {total()}</p>;
}function DebugComponent() {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log("Current count:", count()); // Logs on EVERY change
});
return <button onClick={() => setCount((c) => c + 1)}>Click</button>;
}function ConditionalDisplay() {
const [showDetails, setShowDetails] = createSignal(false);
const [details, setDetails] = createSignal("initial");
createEffect(() => {
if (showDetails()) {
console.log(details()); // NOT tracked when showDetails() is false
}
});
}
// When showDetails is false and details changes, the effect does NOT re-run.
// When showDetails later becomes true, it shows the current details value,
// but missed all intermediate changes.function ConditionalDisplay() {
const [showDetails, setShowDetails] = createSignal(false);
const [details, setDetails] = createSignal("initial");
createEffect(() => {
const show = showDetails();
const currentDetails = details(); // ALWAYS tracked
if (show) {
console.log(currentDetails);
}
});
}function DataDisplay() {
const [loading, setLoading] = createSignal(true);
const [data, setData] = createSignal<string | null>(null);
createEffect(() => {
if (loading()) return; // When true, data() is never read
console.log("Data loaded:", data());
});
}
// Effect only tracks loading(). When loading becomes false, it runs and
// tracks data(). But if data changed while loading was true, it was missed.function DataDisplay() {
const [loading, setLoading] = createSignal(true);
const [data, setData] = createSignal<string | null>(null);
createEffect(() => {
const isLoading = loading();
const currentData = data(); // Always tracked
if (isLoading) return;
console.log("Data loaded:", currentData);
});
}function SearchBar() {
const [query, setQuery] = createSignal("");
const currentQuery = query(); // Snapshot: always ""
const handleSearch = () => {
fetch(`/api/search?q=${currentQuery}`); // Always searches ""
};
return (
<div>
<input onInput={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
);
}function SearchBar() {
const [query, setQuery] = createSignal("");
const handleSearch = () => {
fetch(`/api/search?q=${query()}`); // Reads current value
};
return (
<div>
<input onInput={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
);
}function CustomButton(props: { variant: string } & JSX.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button class={`btn-${props.variant}`} {...props} />;
// props includes variant, which may override class or cause unexpected attributes
}import { splitProps } from "solid-js";
function CustomButton(props: { variant: string } & JSX.ButtonHTMLAttributes<HTMLButtonElement>) {
const [local, rest] = splitProps(props, ["variant"]);
return <button class={`btn-${local.variant}`} {...rest} />;
}function TodoList(props: { todos: Todo[] }) {
return (
<ul>
{props.todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// Every array change recreates ALL <li> elements from scratch.import { For } from "solid-js";
function TodoList(props: { todos: Todo[] }) {
return (
<ul>
<For each={props.todos} fallback={<li>No todos</li>}>
{(todo, index) => (
<li>#{index() + 1}: {todo.text}</li>
)}
</For>
</ul>
);
}
// Only changed/added/removed items update. Existing DOM nodes are reused.import { Index } from "solid-js";
function TagList(props: { tags: string[] }) {
return (
<ul>
<Index each={props.tags}>
{(tag, i) => <li>{tag()}</li>}
</Index>
</ul>
);
}
// Note: in Index, item is a signal (tag()), index is a plain number.function AuthView() {
const [loggedIn, setLoggedIn] = createSignal(false);
return (
<div>
{loggedIn() ? <Dashboard /> : <LoginForm />}
</div>
);
}import { Show } from "solid-js";
function AuthView() {
const [loggedIn, setLoggedIn] = createSignal(false);
return (
<div>
<Show when={loggedIn()} fallback={<LoginForm />}>
<Dashboard />
</Show>
</div>
);
}function PageContent(props: { page: string }) {
switch (props.page) {
case "home": return <Home />;
case "about": return <About />;
case "contact": return <Contact />;
default: return <NotFound />;
}
}
// Component body runs ONCE. If props.page changes, this switch never re-runs.import { Switch, Match } from "solid-js";
function PageContent(props: { page: string }) {
return (
<Switch fallback={<NotFound />}>
<Match when={props.page === "home"}><Home /></Match>
<Match when={props.page === "about"}><About /></Match>
<Match when={props.page === "contact"}><Contact /></Match>
</Switch>
);
}<For each={items()}>
{(item) => <div key={item.id}>{item.name}</div>}
</For>
// The key prop has no effect. For tracks by reference, not by key.<For each={items()}>
{(item) => <div>{item.name}</div>}
</For>import { useRef, useEffect } from "react";
function Canvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d");
ctx?.fillRect(0, 0, 100, 100);
}, []);
return <canvas ref={canvasRef} />;
}import { onMount } from "solid-js";
function Canvas() {
let canvasRef!: HTMLCanvasElement; // Definite assignment assertion
onMount(() => {
const ctx = canvasRef.getContext("2d"); // Direct access, no .current
ctx.fillRect(0, 0, 100, 100);
});
return <canvas ref={canvasRef} />;
}// React: JSX compiles to React.createElement("div", { class: "x" }, children)
// This creates a virtual DOM node that gets diffed and reconciled.
// Attempting manual element creation:
const element = React.createElement("div", null, "Hello");// SolidJS: JSX compiles to direct DOM creation.
// There is no virtual DOM. No diffing. No reconciliation.
// Simply use JSX:
const element = <div>Hello</div>; // Creates actual DOM nodefunction Wrapper(props: { children: JSX.Element }) {
createEffect(() => {
console.log(props.children); // May re-create children!
});
return <div class="wrapper">{props.children}</div>; // May re-create again!
}function Wrapper(props: { children: JSX.Element }) {
const kids = props.children; // Captured, may not be stable
return <div>{kids}</div>;
}import { children } from "solid-js";
function Wrapper(props: { children: JSX.Element }) {
const resolved = children(() => props.children);
createEffect(() => {
console.log(resolved()); // Stable, cached reference
});
return <div class="wrapper">{resolved()}</div>;
}// React: cleanup is the return value of useEffect
useEffect(() => {
const timer = setInterval(tick, 1000);
return () => clearInterval(timer); // Cleanup via return
}, []);createEffect(() => {
const timer = setInterval(tick, 1000);
return () => clearInterval(timer); // This does NOT register cleanup!
// The return value becomes the "prev" value for the next effect run.
});import { createEffect, onCleanup } from "solid-js";
createEffect(() => {
const timer = setInterval(tick, 1000);
onCleanup(() => clearInterval(timer)); // Explicit cleanup registration
});import { onMount, onCleanup } from "solid-js";
function Timer() {
onMount(() => {
const timer = setInterval(tick, 1000);
onCleanup(() => clearInterval(timer));
});
return <div>Timer running</div>;
}import { useRouter } from "next/router";
function NavButton() {
const router = useRouter();
return <button onClick={() => router.push("/dashboard")}>Go</button>;
}import { useHistory } from "react-router-dom";
function NavButton() {
const history = useHistory();
return <button onClick={() => history.push("/dashboard")}>Go</button>;
}import { useNavigate } from "@solidjs/router";
function NavButton() {
const navigate = useNavigate();
return <button onClick={() => navigate("/dashboard")}>Go</button>;
}function UserProfile(props: { id: string }) {
const [user, setUser] = createSignal(null);
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal(null);
createEffect(async () => {
setLoading(true);
try {
const res = await fetch(`/api/users/${props.id}`);
setUser(await res.json());
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
});
return (
<Show when={!loading()} fallback={<p>Loading...</p>}>
<div>{user()?.name}</div>
</Show>
);
}import { createResource, Suspense, Show } from "solid-js";
const fetchUser = async (id: string) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
function UserProfile(props: { id: string }) {
const [user] = createResource(() => props.id, fetchUser);
return (
<Suspense fallback={<p>Loading...</p>}>
<Show when={user()}>
{(u) => <div>{u().name}</div>}
</Show>
</Suspense>
);
}import { query, createAsync } from "@solidjs/router";
import { useParams } from "@solidjs/router";
const getUser = query(async (id: string) => {
"use server";
const res = await fetch(`/api/users/${id}`);
return res.json();
}, "user");
function UserProfile() {
const params = useParams();
const user = createAsync(() => getUser(params.id));
return (
<Suspense fallback={<p>Loading...</p>}>
<div>{user()?.name}</div>
</Suspense>
);
}import { Route } from "@solidjs/router";
<Route path="/dashboard" element={<Dashboard />} />
// element={<Dashboard />} creates the component IMMEDIATELY, not lazily.import { Route } from "@solidjs/router";
<Route path="/dashboard" component={Dashboard} />
// component={Dashboard} passes the reference. Router creates it when needed.// pages/users/[id].tsx (Next.js)
export async function getServerSideProps({ params }) {
const user = await fetchUser(params.id);
return { props: { user } };
}
export default function UserPage({ user }) {
return <div>{user.name}</div>;
}// routes/users/[id].tsx (SolidStart)
import { query, createAsync } from "@solidjs/router";
import { useParams } from "@solidjs/router";
const getUser = query(async (id: string) => {
"use server";
return fetchUser(id);
}, "user");
export default function UserPage() {
const params = useParams();
const user = createAsync(() => getUser(params.id));
return <div>{user()?.name}</div>;
}function TodoForm() {
const [value, setValue] = createSignal("");
const handleSubmit = async (e: Event) => {
e.preventDefault();
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ title: value() }),
});
};
return (
<form onSubmit={handleSubmit}>
<input value={value()} onInput={(e) => setValue(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
// Does not work without JavaScript. No progressive enhancement.import { action } from "@solidjs/router";
const addTodo = action(async (formData: FormData) => {
"use server";
const title = formData.get("title") as string;
await db.addTodo(title);
}, "addTodo");
function TodoForm() {
return (
<form action={addTodo} method="post">
<input name="title" />
<button type="submit">Add</button>
</form>
);
}
// Works WITH and WITHOUT JavaScript. Progressive enhancement by default.