Skip to content

Things Learned Migrating To Solid 2.0

Posted on:March 29, 2026

So the other day I was thinking of what my next blog post should be. I haven’t been writing much and instead have been doing my best to test out the new Solid 2.0 beta as well as upgrade certain libraries in the ecosystem. This has included things like SolidStart, Tanstack Start, as well as Solid Query. Writing about my experience migrating seems to make the most sense since it’s fresh on my mind. It might also be helpful to others looking at migrating their projects.

So what have I learned? I’m glad you asked!

AI helps a ton with migrations

To put it bluntly AI saved me a ton of time. Solid 2.0 is a pretty major departure from 1.0 in a lot of ways. I feel migrations like this used to take months and now they take weeks. In a matter of a week or two I had WIP prs up for both Tanstack Start and SolidStart. This isn’t to say this code wasn’t without its faults but it at least allowed me to forge the path and find bugs or edge cases that can only come from upgrading real projects.

What helped quite significantly was creating a local reference folder with clones of the Solid source code and the signals repo. This gives the model more context when it needs to understand the shape of the new APIs. Even without this newer models are surprisingly good at digging through node_modules, but I still like having the important repos in context.

With this in place, i’ve found that the AI can do a createEffect migration pretty well. It can figure out that createEffect now takes 2-4 arguments instead of just the 1. It can also detect which signals are being tracked and put that in the first half of the effect.

Another thing I have found super valuable to keep the AI on track is a test suite. Tanstack Router has a really good suite of unit and end-to-end tests. When doing a migration this large, feedback like this is a must. I can have the AI iterate and get the tests passing. Without this it tends to go in circles and in some cases make things even worse.

This new technology also lets me work in code I don’t fully understand (for better or worse). For example, I really have no business in the @solidjs/signals repo, but with AI I can at least narrow things down to a specific change. Now AI usually doesn’t have the correct solution to the problem, but it can at least give someone with more knowledge than me a starting point.

Solid 2.0 feels really good

The other big takeaway is that Solid 2.0 feels really good to use, especially the async story. Being able to pass a promise into createMemo and have the system handle it in a sane way feels great. It feels like the framework is meeting you closer to how you already think about async code.

The old Suspense experience could feel a little jarring when it ripped the UI away from you the moment something upstream needed to load. It worked, but it could make even relatively small async interactions feel more dramatic than they needed to. In Solid 2.0, transitions feel like much less of a thing you have to think about manually. Things entangle naturally, pending state composes more cleanly, and the UI tends to stay in place while work is happening instead of constantly snapping between fallback and content.

That shift makes the whole model feel calmer. You can spend less time managing the mechanics of loading and more time deciding what should be pending, what should stay visible, and what the user actually needs in the moment.

Migration tips

So you’ve made it this far and you want to try out the new beta. Below I list some tips that would have saved me a lot of time if I had known them.

Testing-library workaround

Because this is still early, some packages have not fully moved over yet. Sometimes that is a hard blocker, and sometimes you can work around it. One example for me was @solidjs/testing-library. In a Vite setup I needed some combination of the following:

import { defineConfig } from "vite";
import solid from "vite-plugin-solid";

export default defineConfig({
  plugins: [solid({ ssr: true, hot: !process.env.VITEST })],
  resolve: {
    alias: {
      "solid-js/web": "@solidjs/web",
      "solid-js/store": "solid-js",
    },
  },
  environments: {
    ssr: {
      resolve: {
        noExternal: ["@solidjs/web"],
      },
    },
  },
  optimizeDeps: {
    include: ["@solidjs/testing-library"],
  },
  server: {
    deps: {
      inline: ["@solidjs/testing-library"],
    },
  },
});

I would not treat that as a universal fix, but it is the sort of config surgery you may need while the ecosystem settles.

Dedupe local Solid versions

If you are linking packages locally, you may need to dedupe Solid so you do not accidentally run multiple copies. Sometimes Solid warns you about this directly. Other times you just get weird hydration errors and have to figure it out the hard way.

import { defineConfig } from "vite";

export default defineConfig({
  resolve: {
    dedupe: [
      "solid-js",
      "@solidjs/web",
      "@solidjs/router",
      "@solidjs/signals",
      "@solidjs/meta",
    ],
  },
});

Use the next tag

When upgrading the beta, using the next tag makes life a lot easier. From there you can usually run pnpm update solid-js @solidjs/web vite-plugin-solid to move to the latest published beta. If you need a newer @solidjs/signals version before a full Solid release lands, a temporary pnpm.overrides entry can help.

{
  "dependencies": {
    "@solidjs/web": "next",
    "solid-js": "next",
    "vite-plugin-solid": "next"
  },
  "pnpm": {
    "overrides": {
      "@solidjs/signals": "0.13.7"
    }
  }
}

flush() mostly shows up in tests

One gotcha that started showing up for me was flush(). If you set signals and then immediately read from the system, you may not see what you expect yet because the writes are still batched.

import { createSignal, flush } from "solid-js";

const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);

setA(10);
setB(20);

// Neither has updated yet because both writes are still batched.
flush();

This has been most noticeable in unit tests for me. E2E tests usually keep working, but lower-level tests may suddenly need a flush() to observe the state you thought you had already updated.

Track before await

This has always been true, but it becomes much more obvious now that memos can be async. If you expect a signal read to track after an await, you are going to have a bad time. Pull the tracked values above the await instead.

import { createMemo, createSignal } from "solid-js";

// Bad: `count()` is read after the await boundary.
const [count, setCount] = createSignal(0);
const double = createMemo(async () => {
  await new Promise(r => setTimeout(r, 2000));
  return count() * 2;
});

// Good: track first, then await.
const [nextCount, setNextCount] = createSignal(0);
const nextDouble = createMemo(async () => {
  const c = nextCount();
  await new Promise(r => setTimeout(r, 2000));
  return c * 2;
});

Lastly, pay attention to Solid warnings in development. They are often the first sign of a subtle bug. Warnings like reactive read or written in owned scope are usually worth stopping and understanding rather than ignoring.

Conclusion

Overall, I have come away really impressed with Solid 2.0. The async model feels better, the reactivity changes feel thoughtful, and once you get your head around the new mental model it is a very nice upgrade.

The hard part is not really whether Solid 2.0 is worth it. It is more that early migrations come with ecosystem lag, rough edges, and a few easy-to-miss reactivity gotchas. Still, the migration feels very doable, and AI genuinely made the process faster.

If you have been trying the migration yourself, I would love to hear how it went. If you get stuck, feel free to join the Solid Discord.