SolidJS Three Weeks Later: What We Learned

The patterns, anti-patterns, and surprises we discovered after living with SolidJS in production for three weeks.

SolidJS Three Weeks Later: What We Learned

Three weeks ago, we shipped our SolidJS migration. 139 files converted. 7.6x faster script execution. A phone that no longer heats up in your pocket.

The migration post went viral. Now here’s what we didn’t know when we wrote it.

The Mental Model Takes Time

We thought we understood SolidJS after the migration. We didn’t. The first two weeks were a continuous lesson in how deeply React patterns had embedded themselves in our thinking.

The biggest insight: SolidJS isn’t a framework you use. It’s a way of thinking about UI that happens to have a framework attached. Once that clicked, everything got easier.

Anti-Patterns We Discovered (And Fixed)

Three days after launch, we ran a comprehensive audit. Found 27 anti-patterns across 19 files. Here’s what kept biting us:

The splitProps Trap

We knew you couldn’t destructure props. We documented it in the original post. But somehow, this pattern kept sneaking back in:

// We wrote this (wrong)
function Badge({ class: className, variant, ...props }) {
  return <div class={cn("base", className)} {...props} />;
}

// Should have been this
function Badge(props) {
  const [local, rest] = splitProps(props, ["class", "variant"]);
  return <div class={cn("base", local.class)} {...rest} />;
}

Seven UI components had this bug. They worked fine in basic testing because parent props weren’t changing. The moment we had dynamic props, reactivity broke.

React’s Timing Workarounds Don’t Translate

This was subtle. In React, you sometimes need setTimeout(fn, 0) or Promise.resolve().then() to batch state updates or escape race conditions. We had six hooks using these patterns.

They’re unnecessary in SolidJS. State updates are synchronous. The timing workarounds were actually causing bugs because they deferred updates that should have been immediate.

The fix was simple: delete the workarounds. For cases that genuinely needed atomic updates, we used SolidJS’s batch():

// Before: Promise microtask delay (React pattern)
Promise.resolve().then(() => {
  setNodeData(node);
  openModal();
});

// After: SolidJS batch for atomic updates
batch(() => {
  context.setNodeData(node);
  context.setIsOpen(true);
});
openModal();

Missing Cleanup Handlers

Effects in SolidJS need explicit cleanup. We missed cleanup handlers in four files, creating potential memory leaks:

// Audio event listeners without cleanup (leaked)
newAudio.addEventListener("playing", () => setIsPlaying(true));

// Fixed: named handlers with onCleanup
const onPlaying = () => setIsPlaying(true);
newAudio.addEventListener("playing", onPlaying);
onCleanup(() => newAudio.removeEventListener("playing", onPlaying));

Same pattern appeared with setTimeout, requestAnimationFrame, and WebSocket connections. Every resource that lives beyond the current render needs onCleanup().

The Collapsible Cards Bug That Taught Us Everything

This one deserves its own section because it encapsulates the SolidJS mental model.

We had collapsible cards. Click one, it expands. Click another, that one expands too. Simple UI. Except clicking any card expanded all of them.

We tried four different approaches before finding the solution:

Attempt 1: createSignal with Set

const [expanded, setExpanded] = createSignal<Set<string>>(new Set());

Didn’t work. Sets don’t play well with SolidJS fine-grained reactivity.

Attempt 2: createSignal with Record

const [expanded, setExpanded] = createSignal<Record<string, boolean>>({});

Still broken. createSignal tracks the entire object reference. Changing any key triggers all subscribers.

Attempt 3: createStore with Record

const [expanded, setExpanded] = createStore<Record<string, boolean>>({});

Should have worked. Didn’t. Turns out our API was returning duplicate IDs for different cards.

Attempt 4: createStore with boolean[]

const [expanded, setExpanded] = createStore<boolean[]>([]);
const toggle = (i: number) => setExpanded(i, (prev) => !prev);

Finally worked. Array index guarantees uniqueness. No dependency on data quality.

But here’s the real lesson: we also had a helper function breaking reactivity:

// This looks cleaner but breaks reactive tracking
const isExpanded = (id: number) => expanded[id];

// Store access must happen directly in JSX
const Example = () => <div class={expanded[index()] ? "open" : "closed"} />;

In SolidJS, store property access creates reactive subscriptions only when it happens in a reactive context (JSX, effects, memos). A helper function runs outside that context.

Code Splitting: 56% Bundle Reduction

A day after launch, we realized we hadn’t code-split anything. The entire app was a single 1,335 KB bundle. It performed well, but we could do better.

We lazy-loaded five heavy modal components:

const MemoryNetwork = lazy(() => import("@/components/modals/MemoryNetwork"));
const Settings = lazy(() => import("@/components/modals/Settings"));
// ... three more

Results:

MetricBeforeAfter
Main bundle1,335 KB591 KB
vis-networkIn main558 KB separate chunk
Gzip main~400 KB182 KB

The vis-network library (our graph visualization) was 558 KB sitting in the main bundle. Users who never opened the memory network were downloading it anyway.

We added background prefetching so modals feel instant when opened:

export function prefetchModals() {
  const prefetch = () => {
    import("@/components/modals/Settings");
    import("@/components/modals/MemoryNetwork");
    // ...
  };

  if ("requestIdleCallback" in window) {
    requestIdleCallback(prefetch);
  } else {
    setTimeout(prefetch, 100); // Safari fallback
  }
}

Chunks load during browser idle time. When users click, the modal appears immediately because it’s already cached.

The Patterns That Stuck

After three weeks, here’s what we’ve internalized:

1. Use createStore for lists, createSignal for primitives. If you need fine-grained reactivity over individual items, you need a store. Signals track the whole value.

2. Access store properties directly in JSX. No helper functions. No intermediary variables. The reactive subscription happens at the point of access.

3. splitProps for every component that forwards props. It’s more verbose than destructuring. It’s also correct.

4. onCleanup for everything. Timers, event listeners, animation frames, subscriptions. If it outlives the render, clean it up.

5. batch for atomic updates, not timing hacks. When you need multiple state changes to appear as one, use batch(). Delete the setTimeout(fn, 0) patterns.

What We’d Tell Past Us

If we were starting the migration today:

  1. Run the anti-pattern audit immediately after migration, not a week later. The bugs were subtle. They didn’t crash the app. They just made reactivity silently fail in edge cases.

  2. Code split from day one. It’s trivial in SolidJS with lazy(). There’s no reason to ship a 1.3 MB bundle when 600 KB works fine.

  3. Write a solidjs-patterns skill file. We eventually created one and open-sourced it. Should have done it during migration. Having documented patterns prevents regression. Install it with npx skills add https://github.com/omniaura/skills --skill solidjs-patterns.

  4. Test collapsible/expandable UI obsessively. This pattern is where SolidJS reactivity is most likely to surprise you. If you have lists with individual item state, test every combination.

The Performance Held Up

We’ve been watching the metrics. The numbers from the original post are holding:

  • FCP still averaging 330ms (vs 576ms with React)
  • Memory still 30-35 MB (vs 40-67 MB with React)
  • Phone still doesn’t heat up

No performance regressions. No memory leaks. The production stress test has been three weeks of real users.

Was It Worth It?

Absolutely.

The migration itself was the easy part. Living with SolidJS taught us its actual shape. The mental model is genuinely different from React, and that difference compounds over time.

Our codebase is smaller. Our bundles are smaller. Our users’ phones stay cooler. And every new component we write is faster to build because we’re not fighting the framework.

Would we do it again? We already have. Every new feature we’ve shipped in the past three weeks has been SolidJS from the start. And we haven’t looked back.


The full technical reports from our post-migration work are in our internal docs. If you’re considering a similar migration and want to compare notes, reach out.

We’ve open-sourced our SolidJS patterns as a skill for AI coding agents:

npx skills add https://github.com/omniaura/skills --skill solidjs-patterns

— Peyton