Back

Best Practices for Working with SolidJS

Best Practices for Working with SolidJS

SolidJS delivers near-native DOM performance through fine-grained reactivity, but that same model introduces patterns that trip up developers coming from React or Vue. This article covers the SolidJS best practices that matter most in real applications — the ones that prevent subtle bugs and keep your code predictable.

Key Takeaways

  • SolidJS components execute once as setup functions — reactivity lives at the signal level, not the component level
  • Keep signal reads inside reactive scopes: JSX expressions, createEffect, or createMemo
  • Derive values with functions or createMemo instead of syncing state through effects
  • Never destructure props — use mergeProps and splitProps to preserve the getter chain
  • Use createStore for nested state and createResource for async data fetching

Understand That Components Run Once

This is the mental model shift everything else depends on. In React, components re-render when state changes. In SolidJS, components execute once as setup functions. Reactivity happens at the signal level, not the component level.

That means this is broken:

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = count() * 2 // Runs once. Never updates.
  return <div>{doubled}</div>
}

The fix is to keep signal reads inside reactive scopes — JSX expressions, createEffect, or createMemo:

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = createMemo(() => count() * 2) // Tracks count reactively
  return <div>{doubled()}</div>
}

SolidJS Reactivity Patterns: Signals, Memos, and Effects

Fine-grained reactivity in SolidJS is built on three primitives. Knowing when to use each one is central to writing efficient SolidJS components.

  • createSignal — for primitive values and simple state
  • createMemo — for derived values that depend on signals
  • createEffect — for side effects only (DOM manipulation, third-party libraries)

The most common mistake is using createEffect to synchronize state:

// ❌ Anti-pattern: effect used for derivation
createEffect(() => setFullName(`${firstName()} ${lastName()}`))

// ✅ Correct: derive directly
const fullName = () => `${firstName()} ${lastName()}`

A plain function like fullName works because SolidJS re-evaluates it whenever it appears inside a reactive scope that reads its underlying signals. Reach for createMemo only when the derivation is expensive and you want to cache the result.

Reserve createEffect for work outside Solid’s reactive graph — things like initializing a chart library or imperatively updating a DOM element. See the official explanation of memos and derived values for deeper details.

Never Destructure Props

SolidJS props are backed by getters. Destructuring them breaks the reactive connection:

// ❌ Breaks reactivity
function User({ name }: { name: string }) {
  return <h1>{name}</h1>
}

// ✅ Preserves reactivity
function User(props: { name: string }) {
  return <h1>{props.name}</h1>
}

When you need default values or want to separate props you consume from props you forward, use mergeProps and splitProps — both preserve the getter chain.

Use Control Flow Components for Conditional and List Rendering

SolidJS performance patterns depend on using the right rendering primitives. Prefer Solid’s control-flow components for reactive conditionals and lists rather than relying on raw JavaScript logic inside JSX.

// ✅ Conditional rendering
<Show when={isLoggedIn()} fallback={<LoginPage />}>
  <Dashboard />
</Show>

// ✅ List rendering — preserves item identity across updates
<For each={posts()}>{(post) => <PostCard post={post} />}</For>

Use <Index> instead of <For> when list positions remain stable but values at those positions may change. <For> tracks items by reference and works best for arrays of objects with stable identity, while <Index> tracks items by position.

You can read more about Solid’s list rendering primitives in the official docs.

Use Stores for Complex State

Signals track changes to a single value. When that value is a large nested object, updates replace the entire value, which means all consumers of that signal react to the change. createStore provides property-level reactivity that works better for nested state:

const [state, setState] = createStore({ user: { name: "Alice" }, posts: [] })

// Only components reading state.posts react to this
setState("posts", (p) => [...p, newPost])

Use produce for complex mutations and reconcile when merging server data into an existing store.

Handling Async Data the Right Way

Fetching inside createEffect can lead to race conditions and doesn’t integrate cleanly with Suspense. Use createResource instead:

const [posts] = createResource(() => fetch("/api/posts").then((r) => r.json()))

createResource accepts an optional source signal as its first argument. When provided, the fetcher re-runs whenever that source changes, and the resource automatically integrates with Solid’s <Suspense> and <ErrorBoundary> components.

In SolidStart apps, use query and createAsync together with preload functions. Preload functions can run during navigation intent (such as link hover) and again during navigation, allowing data to be ready by the time the component renders.

Conclusion

SolidJS rewards developers who work with its reactivity model rather than against it. Keep signal reads inside reactive scopes, derive values instead of syncing them with effects, never destructure props, and reach for createStore when state has nested structure. These SolidJS reactivity patterns aren’t arbitrary rules — they’re the direct consequence of how fine-grained reactivity works, and following them produces components that are both correct and fast.

FAQs

SolidJS components run once, so any signal read assigned to a plain variable outside a reactive scope captures only the initial value. Wrap the derivation in createMemo or place the signal read directly inside JSX. Both are reactive scopes that re-evaluate whenever the underlying signal changes.

A plain derived function works fine for lightweight computations because SolidJS re-evaluates it each time a consuming reactive scope runs. Use createMemo when the derivation is expensive or read by multiple consumers. createMemo caches the result and only recalculates when its dependencies change.

For tracks each item by reference, so it is ideal for arrays of objects with stable identity. Index tracks items by their position in the array, making it better suited for lists where the position remains stable but the value at that position may change.

You can, but you should not. Using createEffect to sync derived state creates unnecessary intermediate updates and can introduce glitches. Instead, derive the value with a function or createMemo. Reserve createEffect for true side effects like DOM manipulation, logging, or interacting with third-party libraries.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay