Skip to content

Latest commit

 

History

History
280 lines (223 loc) · 11.1 KB

File metadata and controls

280 lines (223 loc) · 11.1 KB
name solid-errors-react-contamination
description Use when reviewing SolidJS code for React anti-patterns or converting React components to SolidJS. Prevents destructured props, useState/useEffect habits, Array.map rendering, and all other React patterns that silently break SolidJS reactivity. Covers all 12+ contamination patterns including props destructuring, signal misuse, effect cleanup, list rendering, conditional rendering, refs, children, and navigation. Keywords: React to SolidJS, destructure props, createSignal, createEffect, For component, Show component, anti-pattern, migration, convert from React, React habits, why does my component break, coming from React.
license MIT
compatibility Designed for Claude Code. Requires SolidJS 1.x/2.x with TypeScript.
metadata
author version
OpenAEC-Foundation
1.0

solid-errors-react-contamination

Critical Warnings

NEVER destructure props in SolidJS components. Destructuring severs the reactive proxy connection. The component function runs ONCE — destructured values are frozen snapshots that NEVER update. This is the #1 source of broken SolidJS code generated by AI assistants trained on React.

NEVER assume components re-render. SolidJS component functions execute exactly ONCE to set up the reactive graph. Code in the component body that depends on state changes (derived values, conditionals, logging) MUST be wrapped in reactive primitives (createMemo, createEffect) or inline JSX expressions.

NEVER use dependency arrays. SolidJS tracks dependencies automatically. There is no [deps] argument to createEffect or createMemo. Adding one is a syntax error or passes it as the initial value parameter.

NEVER return cleanup functions from effects. SolidJS uses onCleanup() as a separate call inside the effect, NOT a return value.

NEVER use Array.map() for list rendering. It recreates ALL DOM nodes on every array change. ALWAYS use <For> or <Index> components.

Quick Reference: All Anti-Patterns

ID Pattern Severity Detection
AP-001 Destructuring props CRITICAL function X({ prop } or const { x } = props
AP-002 Destructuring signal value CRITICAL const val = signal() outside JSX/effect
AP-003 useState instead of createSignal CRITICAL useState import or usage
AP-004 useEffect instead of createEffect CRITICAL useEffect import or [deps] array
AP-005 useMemo instead of createMemo HIGH useMemo import or dependency array
AP-006 Re-render assumption CRITICAL Derived values as plain variables in component body
AP-007 Conditional signal access HIGH Signal read inside if in effect
AP-008 Early return before signal access HIGH return before signal call in effect
AP-009 Storing signal in variable HIGH const x = signal() in component body
AP-010 Spreading props unsafely MEDIUM {...props} without splitProps
AP-011 Array.map for lists HIGH .map() in JSX return
AP-012 Ternary instead of Show MEDIUM {cond ? <A/> : <B/>}
AP-013 switch/case in component body HIGH switch statement in component return
AP-014 key prop on list items LOW key={...} prop in For callback
AP-015 useRef instead of let ref MEDIUM useRef import or .current access
AP-016 React.createElement assumption LOW Manual element creation calls
AP-017 children as static value HIGH props.children without children() helper
AP-018 useEffect cleanup return CRITICAL return () => cleanup in effect
AP-019 useRouter / next/router HIGH useRouter import
AP-020 useEffect for data fetching HIGH fetch inside useEffect/createEffect
AP-021 element prop on Route HIGH element={<Component/>} on Route
AP-022 getServerSideProps pattern HIGH Separate data-fetching exports
AP-023 Form onSubmit with preventDefault MEDIUM e.preventDefault() in form handler

Top 10 Priority Patterns (Detailed)

AP-001: Destructuring Props (CRITICAL)

// WRONG -- React pattern: destructuring kills reactive tracking
function Greeting({ name }: { name: string }) {
  return <h1>Hello {name}</h1>; // NEVER updates
}

// WRONG -- Same problem, different syntax
function Greeting(props: { name: string }) {
  const { name } = props; // Snapshot, frozen forever
  return <h1>Hello {name}</h1>;
}

// CORRECT -- Access props object directly
function Greeting(props: { name: string }) {
  return <h1>Hello {props.name}</h1>; // Reactive, updates on change
}

// CORRECT -- Use splitProps when separating concerns
function Greeting(props: { name: string; class?: string }) {
  const [local, rest] = splitProps(props, ["name"]);
  return <h1 {...rest}>Hello {local.name}</h1>;
}

Why it breaks: Props are reactive getters on a proxy. Destructuring reads the value once and discards the proxy connection.

AP-002: Destructuring Signal Value (CRITICAL)

// WRONG -- Snapshot, never updates
const [count, setCount] = createSignal(0);
const value = count(); // Frozen at 0
return <div>{value}</div>;

// CORRECT -- Call getter in JSX expression
return <div>{count()}</div>;

Why it breaks: Calling count() outside a tracking scope captures a static value. Inside JSX, the compiler wraps it in a reactive effect.

AP-003: useState vs createSignal (CRITICAL)

// WRONG -- React API does not exist in SolidJS
const [count, setCount] = useState(0);
return <div>{count}</div>; // count is a value in React

// CORRECT -- SolidJS returns a getter FUNCTION
const [count, setCount] = createSignal(0);
return <div>{count()}</div>; // MUST call count()

Why it breaks: createSignal returns [getter, setter] where getter is a function. Forgetting () renders the function object, not the value.

AP-004: useEffect vs createEffect (CRITICAL)

// WRONG -- Dependency array pattern
useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);

// CORRECT -- Automatic tracking, no dependency array
createEffect(() => {
  document.title = `Count: ${count()}`;
});

Why it breaks: SolidJS has NO dependency array concept. The second argument to createEffect is an initial value for the previous-value parameter, NOT a dependency list.

AP-005: useMemo vs createMemo (HIGH)

// WRONG -- React pattern with dependency array
const double = useMemo(() => count * 2, [count]);

// CORRECT -- Auto-tracked, IS a reactive source
const double = createMemo(() => count() * 2);
// Use as: double() -- it's a getter function

Why it breaks: React's useMemo is NOT a reactive source. SolidJS's createMemo IS -- other computations can track it.

AP-006: Component Re-Render Assumption (CRITICAL)

// WRONG -- Expects component body to re-run
function Counter() {
  const [count, setCount] = createSignal(0);
  const doubled = count() * 2; // Computed ONCE, frozen
  console.log("render"); // Logs ONCE, not on updates
  return <p>{doubled}</p>;
}

// CORRECT -- Use derived function or memo
function Counter() {
  const [count, setCount] = createSignal(0);
  const doubled = createMemo(() => count() * 2);
  return <p>{doubled()}</p>;
}

Why it breaks: The component function is a setup function. It runs once. Only reactive expressions (effects, memos, JSX bindings) re-execute.

AP-007: Conditional Signal Access (HIGH)

// WRONG -- Signal only tracked when condition is true
createEffect(() => {
  if (isEnabled()) {
    console.log(data()); // NOT tracked when isEnabled() is false
  }
});

// CORRECT -- Access all signals before conditions
createEffect(() => {
  const enabled = isEnabled();
  const currentData = data(); // ALWAYS tracked
  if (enabled) {
    console.log(currentData);
  }
});

Why it breaks: SolidJS tracks signals at read time. If a branch prevents reading a signal, that signal is not registered as a dependency for that execution.

AP-008: Early Return Before Signal Access (HIGH)

// WRONG -- Signals after return are never tracked
createEffect(() => {
  if (loading()) return;
  console.log(name()); // Never tracked when loading is true
});

// CORRECT -- Read all signals first
createEffect(() => {
  const isLoading = loading();
  const currentName = name();
  if (isLoading) return;
  console.log(currentName);
});

AP-009: Storing Signal in Variable (HIGH)

// WRONG -- Stale snapshot
function Timer() {
  const [count, setCount] = createSignal(0);
  const current = count(); // Snapshot: always 0

  setInterval(() => {
    console.log(current); // Always logs 0
  }, 1000);

  return <div>{current}</div>; // Always shows 0
}

// CORRECT -- Call getter when needed
function Timer() {
  const [count, setCount] = createSignal(0);
  return <div>{count()}</div>; // Reactive
}

AP-010: Spreading Props Unsafely (MEDIUM)

// WRONG -- May break tracking for dynamic props
function Button(props: ButtonProps) {
  return <button {...props}>{props.children}</button>;
}

// CORRECT -- Use splitProps for controlled spreading
function Button(props: ButtonProps) {
  const [local, rest] = splitProps(props, ["children", "onClick"]);
  return <button onClick={local.onClick} {...rest}>{local.children}</button>;
}

Decision Tree: Is This a React Anti-Pattern?

Code contains signal/props access?
├── Props destructured in function signature? --> AP-001 (CRITICAL)
├── Props destructured with const { } = props? --> AP-001 (CRITICAL)
├── Signal value stored in const outside JSX? --> AP-002/AP-009 (CRITICAL/HIGH)
├── useState / useEffect / useMemo import? --> AP-003/004/005 (CRITICAL)
├── Dependency array [deps] on effect/memo? --> AP-004/005 (CRITICAL/HIGH)
├── Component body has derived non-reactive values? --> AP-006 (CRITICAL)
├── Effect has early return before signal reads? --> AP-008 (HIGH)
├── Effect has conditional signal access? --> AP-007 (HIGH)
├── Return value from effect for cleanup? --> AP-018 (CRITICAL)
├── Array.map() in JSX? --> AP-011 (HIGH)
├── key={} prop on elements? --> AP-014 (LOW)
├── useRef or .current access? --> AP-015 (MEDIUM)
├── props.children used without children() helper? --> AP-017 (HIGH)
└── useRouter import? --> AP-019 (HIGH)

Reference Links

Official Sources