Migrating from Next.js to SolidStart: An Opinionated Guide

A practical guide to migrating from Next.js to SolidStart - focusing on the primitives, the mental model shift, and why it might be worth your time.

24 min read
hero image

Given my previous thoughts on the state of things, and my love of all things Solid, it feels like the right time to help people who might be looking for alternatives to Next.js. Whether your reasons are ideological, practical, or you’re just curious - this guide is for you.

I’m of the opinion that SolidStart is one of the best options out there. V2 is in late alpha and already being deployed in production by early adopters - it uses Vite’s Environment API to replace Vinxi with a leaner, more debuggable stack. (Nitro 3 integration is still in progress as a separate effort.) The migration from v1 is minimal (config changes, mostly - your component code and server functions stay the same). This guide covers the fundamentals that apply to both versions.

But before we get into the metaframework stuff, we need to talk about the elephant in the room: Solid is not React. It looks like React, and that’s both a blessing and a trap.

The one thing you need to internalize

Here it is. The single most important mental model shift:

In Solid, the component function is a constructor, not a render function.

In React, your component function re-runs on every state change. Every. Single. Time. Hooks, variables, derived values - all re-evaluated. In Solid, the component function runs once. It sets things up, wires the reactive graph together, and never runs again. All subsequent updates happen through signal subscriptions that surgically update the specific DOM nodes that care about the change.

Once you truly get this, every other difference clicks into place. Signals are getter functions because you need live references. Effects auto-track because they subscribe at call-time. Props can’t be destructured because that reads values eagerly outside a tracking scope. <Show> and <For> exist because there’s no virtual DOM to diff.

Let that sink in. Now let’s get practical.

Solid vs. React: the primitives

On the surface, Solid and React feel similar - which is a good thing if you’re coming from React land. Both use JSX to output HTML. But the similarities are surface-level. How they handle state, effects, iteration, conditional rendering, and lifecycle is fundamentally different.

Let’s go through each, side by side.

1. State: useState vs createSignal

React:

function Counter() {
  // This entire function re-runs on every setCount call
  const [count, setCount] = useState(0);
  console.log("rendered"); // fires every time

  return <button onClick={() => setCount(count + 1)}>
    {count}
  </button>;
}

Solid:

function Counter() {
  // This function runs ONCE
  const [count, setCount] = createSignal(0);
  console.log("setup"); // fires exactly once

  return <button onClick={() => setCount(count() + 1)}>
    {count()}
  </button>;
}

The gotcha that’ll bite you first: count is a function in Solid. You call count() to read the value. Writing {count} without the parentheses will render the function reference itself - not the number. This is, by far, the most common mistake React developers make when starting with Solid.

2. Effects: useEffect vs createEffect

React requires you to manually declare dependencies. Solid tracks them automatically.

React:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetchUser(userId).then(data => {
      if (!cancelled) setUser(data);
    });
    return () => { cancelled = true; }; // cleanup via return
  }, [userId]); // manual deps - forget this and you have a bug

  return <div>{user?.name}</div>;
}

Solid:

function UserProfile(props) {
  const [user, setUser] = createSignal(null);

  createEffect(() => {
    // Solid auto-tracks props.userId - no deps array
    const controller = new AbortController();
    fetchUser(props.userId, { signal: controller.signal }).then(setUser);

    onCleanup(() => controller.abort()); // cleanup via onCleanup()
  });

  return <div>{user()?.name}</div>;
}

Key differences to watch for:

  • No deps array. Solid tracks what you read inside the effect automatically. If it reads props.userId, it re-runs when that changes. Period.
  • Cleanup uses onCleanup(), not a return value. Import it from solid-js.
  • No empty deps array pattern. In React, useEffect(() => {...}, []) means “run once on mount.” In Solid, use onMount() for that.
  • Timing is different. createEffect runs synchronously after the reactive graph updates - closer in spirit to React’s useLayoutEffect than useEffect, though the exact timing relative to browser paint depends on when the signal change was triggered. If you need explicit dependency control, Solid has on():
import { on } from "solid-js";

createEffect(on(count, (value, prev) => {
  console.log("count changed from", prev, "to", value);
}));

3. Memos: useMemo vs createMemo

React’s useMemo is a performance optimization, not a semantic guarantee - React may discard cached values. Solid’s createMemo is a reactive primitive that creates a derived signal. It’s guaranteed to be lazy and cached.

React:

function FilteredList({ items, filter }) {
  const filtered = useMemo(
    () => items.filter(i => i.category === filter),
    [items, filter]
  );
  return <ul>{filtered.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Solid:

function FilteredList(props) {
  const filtered = createMemo(() =>
    props.items.filter(i => i.category === props.filter)
  );

  return <ul>
    <For each={filtered()}>{i => <li>{i.name}</li>}</For>
  </ul>;
}

Same deal: createMemo returns a getter function - call filtered(), not filtered.

4. Conditional rendering: ternaries vs <Show>

In React, ternaries work fine because the virtual DOM diffs everything anyway. In Solid, there’s no virtual DOM. Raw ternaries can cause DOM nodes to be torn down and recreated on every toggle. <Show> handles this correctly.

React:

function Greeting({ user }) {
  return (
    <div>
      {user ? (
        <h1>Welcome back, {user.name}!</h1>
      ) : (
        <h1>Please sign in.</h1>
      )}
    </div>
  );
}

Solid:

function Greeting(props) {
  return (
    <div>
      <Show when={props.user} fallback={<h1>Please sign in.</h1>}>
        {(user) => <h1>Welcome back, {user().name}!</h1>}
      </Show>
    </div>
  );
}

The callback child form {(user) => ...} gives you a narrowed, guaranteed-truthy value. But note: user is a getter function in Solid, hence user().name.

For multiple conditions, <Switch> / <Match> is your friend:

<Switch fallback={<p>Not found</p>}>
  <Match when={state() === "loading"}><Spinner /></Match>
  <Match when={state() === "error"}><ErrorView /></Match>
  <Match when={state() === "ready"}><Content /></Match>
</Switch>

5. List rendering: .map() vs <For>

In React, .map() recreates the virtual DOM array every render, then diffs with keys. In Solid, <For> tracks each item by reference - only affected DOM nodes get created, moved, or removed.

React:

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Solid:

function TodoList(props) {
  return (
    <ul>
      <For each={props.todos}>
        {(todo, index) => <li>{todo.text}</li>}
      </For>
    </ul>
  );
}

A couple of things:

  • No key prop needed. <For> tracks by reference automatically.
  • index is a signal. Call index() to get the number. The item itself (todo) is not a signal - it’s the raw value.
  • Using .map() in Solid works - it won’t crash your app - but it creates a non-keyed list that re-renders everything on every update. Essentially, it makes Solid behave like a slow React app. <For> is the correct tool for the job.

6. Refs: useRef vs… just a variable

This one’s refreshingly simple.

React:

function AutoFocus() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

Solid:

function AutoFocus() {
  let inputRef;

  onMount(() => {
    inputRef.focus(); // no .current - it IS the element
  });

  return <input ref={inputRef} />;
}

No .current indirection. No hook. Just a variable. Since Solid components run once, a plain let persists for the component’s lifetime. This also means you don’t need useRef as a “mutable box” - any let variable already serves that purpose.

7. Props: the destructuring trap

This is the gotcha that bites hardest, because it works perfectly in React.

React - destructuring is totally safe:

function Greeting({ name, age }) {
  return <p>{name} is {age} years old</p>;
}

Solid - destructuring BREAKS reactivity:

// BAD - values captured once, never update
function Greeting({ name, age }) {
  return <p>{name} is {age} years old</p>;
}

// GOOD - reactive access through the props proxy
function Greeting(props) {
  return <p>{props.name} is {props.age} years old</p>;
}

Why? Solid props are a Proxy. Accessing props.name inside JSX creates a reactive subscription. Destructuring eagerly reads the values during component setup (which runs once), severing the reactive connection forever.

When you need defaults or to split props for forwarding, use mergeProps and splitProps:

import { splitProps, mergeProps } from "solid-js";

function Button(props) {
  const merged = mergeProps({ variant: "primary", size: "md" }, props);
  const [local, rest] = splitProps(merged, ["variant", "size"]);

  return (
    <button class={`btn-${local.variant} btn-${local.size}`} {...rest}>
      {props.children}
    </button>
  );
}

Quick reference cheat sheet

ConceptReactSolidGotcha
Stateconst [v, setV] = useState(0)const [v, setV] = createSignal(0)v() not v
EffectsuseEffect(() => {}, [deps])createEffect(() => {})No deps array; onCleanup for cleanup
MemouseMemo(() => x, [deps])createMemo(() => x)Returns getter: memo()
RefsuseRef(null) + .currentlet ref;No .current
Conditionals{cond ? <A/> : <B/>}<Show when={cond}>...</Show>Callback child for narrowing
Listsarr.map(x => <X key={id}/>)<For each={arr}>{x => <X/>}</For>No key; index is a signal
PropsDestructure freelyNever destructureUse splitProps / mergeProps
Component fnRe-runs every renderRuns onceDerived values need () => wrapper

Next.js vs SolidStart: the metaframework layer

With the primitives covered, let’s zoom out. You’re not just migrating from React to Solid - you’re migrating from Next.js to SolidStart. That means routing, SSR, server-side code execution, API endpoints, data fetching, and all the other metaframework concerns. Let’s go through them.

Routing

Both frameworks use file-based routing, but the conventions differ.

Next.js (App Router):

app/
  page.tsx            → /
  blog/
    page.tsx          → /blog
    [slug]/
      page.tsx        → /blog/:slug
  (marketing)/
    about/
      page.tsx        → /about
  layout.tsx          → root layout
  loading.tsx         → loading UI
  error.tsx           → error boundary

Next.js uses special file conventions - page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx - each with specific roles. Route groups use parentheses (group). Layouts are implicit: a layout.tsx wraps all pages in its directory and below. Dynamic segments use [param], catch-all uses [...param].

SolidStart:

routes/
  index.tsx           → /
  blog/
    index.tsx         → /blog
    [slug].tsx        → /blog/:slug
  about.tsx           → /about
  blog.tsx            → layout for /blog/*

SolidStart keeps it simpler. A file’s name is the route - no page.tsx convention. Dynamic segments use [param], optional params use [[param]], catch-all uses [...param]. Layouts work by naming a file the same as a directory - blog.tsx alongside a blog/ folder makes blog.tsx the layout. Child routes render via props.children:

// routes/blog.tsx - layout for all /blog/* routes
export default function BlogLayout(props) {
  return (
    <div class="blog-wrapper">
      <nav>Blog nav here</nav>
      {props.children}
    </div>
  );
}

Route groups use parentheses (group)/, same idea as Next.js - directories wrapped in () that organize routes without affecting URL structure.

The key difference: Next.js has more special files with implicit behavior (loading states, error boundaries, not-found pages are all file-convention driven). SolidStart gives you the routing structure and lets you compose <Suspense>, <ErrorBoundary>, and <Show> yourself. Fewer conventions to memorize, more explicit control.

SSR and rendering modes

Next.js gives you a buffet of rendering strategies:

  • Static (SSG) - pages pre-rendered at build time
  • Server-side (SSR) - rendered on each request
  • Incremental Static Regeneration (ISR) - static pages that revalidate after a set time
  • Partial Pre-rendering (PPR) - static shell with streaming dynamic holes (React 19.2)
  • React Server Components - components that run on the server and ship zero client JS

The rendering mode is determined by what you do in the component. Use "use cache"? Cached. Read cookies() or headers()? Dynamic. It’s implicit, which can be surprising - adding a single cookies() call can flip an entire page from static to dynamic.

SolidStart is more straightforward:

  • SSR is on by default (ssr: true). Every route is server-rendered.
  • SSG/prerendering is opt-in via config:
// app.config.ts
export default defineConfig({
  server: {
    prerender: {
      routes: ["/", "/about"],
      // or: crawlLinks: true
    },
  },
});
  • SPA mode if you want it: ssr: false.
  • Streaming SSR is supported out of the box - <Suspense> boundaries become streaming boundaries automatically.

No ISR, no PPR, no implicit mode switching. You pick a mode and that’s what you get. If you need per-route control, you handle it with server functions and caching primitives, not framework magic.

Server-side code execution

This is where the philosophies diverge most.

Next.js has three main mechanisms for running code on the server:

  1. Server Components (the default in App Router) - your component function runs on the server. It can await data, touch the filesystem, query databases. The rendered output streams to the client as RSC Flight payload. The client never sees the component’s JavaScript.
// This is a Server Component by default
async function ProductPage({ params }) {
  const { id } = await params;
  const product = await db.products.find(id);
  return <ProductDisplay product={product} />;
}
  1. Server Actions - functions marked with "use server" that handle mutations. Invoked from forms or client code, executed on the server.
"use server";
export async function addToCart(formData) {
  const productId = formData.get("productId");
  await db.cart.add(productId);
  revalidatePath("/cart");
}
  1. Route Handlers - traditional API endpoints in app/api/*/route.ts files.

The boundary between server and client is managed by directives: "use client" marks a component as client-side, "use server" marks a function as server-only. The mental model of what runs where is the number one source of confusion in the App Router, per community surveys.

SolidStart uses a single, consistent mechanism: server functions.

Any function annotated with "use server" runs on the server. That’s it. No Server Components, no implicit server/client boundary - you explicitly opt into server execution per function.

// A server function for reading data
const getProduct = query(async (id) => {
  "use server";
  return await db.products.find(id);
}, "product");

// A server function for mutations
const addToCart = action(async (formData) => {
  "use server";
  const productId = formData.get("productId");
  await db.cart.add(productId);
});

On the component side, you consume these with createAsync:

function ProductPage(props) {
  const product = createAsync(() => getProduct(props.params.id));

  return (
    <Show when={product()}>
      {(p) => <ProductDisplay product={p()} />}
    </Show>
  );
}

The difference in mental model is significant. In Next.js, you have to think about which components run where. In SolidStart, all components run in the same context (SSR’d on the server, hydrated on the client) - you only think about which functions run on the server. One boundary to manage instead of two.

Data fetching

Next.js:

// Server Component - just await (server only)
async function Posts() {
  const posts = await db.posts.findMany();
  return <PostList posts={posts} />;
}

// With caching (v16+)
"use cache";
async function CachedPosts() {
  const posts = await db.posts.findMany();
  return <PostList posts={posts} />;
}

// Client-side - use SWR or TanStack Query
"use client";
function LivePosts() {
  const { data } = useSWR("/api/posts", fetcher);
  return <PostList posts={data} />;
}

Next.js has gone through three caching models in three major versions. v14 cached fetch() calls aggressively by default (which confused everyone). v15 removed default caching. v16 introduced "use cache" as an explicit opt-in. On top of that, there are four distinct cache layers: Request Memoization, Data Cache, Full Route Cache, and Router Cache. Understanding when each applies is… non-trivial.

SolidStart:

// Define a cached query with a key
const getPosts = query(async () => {
  "use server";
  return await db.posts.findMany();
}, "posts");

// Preload in route config
export const route = {
  preload: () => getPosts(),
};

// Consume in component
function Posts() {
  const posts = createAsync(() => getPosts());

  return (
    <Suspense fallback={<Loading />}>
      <For each={posts()}>
        {(post) => <PostCard post={post} />}
      </For>
    </Suspense>
  );
}

One caching primitive (query), one consumption primitive (createAsync), explicit cache keys, and <Suspense> for loading states. Mutations go through action(). Cache invalidation targets specific keys. That’s the whole model.

API endpoints

Next.js uses Route Handlers:

// app/api/posts/route.ts
export async function GET(request: Request) {
  const posts = await db.posts.findMany();
  return Response.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.posts.create(body);
  return Response.json(post, { status: 201 });
}

SolidStart uses API routes in the same routes/ directory:

// routes/api/posts.ts
import { type APIEvent } from "@solidjs/start/server";

export async function GET(event: APIEvent) {
  const posts = await db.posts.findMany();
  return Response.json(posts);
}

export async function POST(event: APIEvent) {
  const body = await event.request.json();
  const post = await db.posts.create(body);
  return Response.json(post, { status: 201 });
}

Pretty similar, honestly. Both use the standard Request/Response Web APIs. SolidStart gives you the event object with some extras (locals, request, response headers), but the shape is familiar. The main difference is that SolidStart’s API routes live alongside your page routes - no separate api/ directory convention needed (though you can organize them that way if you want).

Middleware

Next.js 16 introduced proxy.ts as the recommended path for heavy server-side logic - auth, redirects, rewrites - running on the Node.js runtime. The old middleware.ts isn’t fully deprecated, but it’s now specialized for Edge Runtime only. For self-hosted setups (think Hetzner, Coolify, Docker), proxy.ts is what you want.

SolidStart has createMiddleware:

import { createMiddleware } from "@solidjs/start/middleware";

export default createMiddleware({
  onRequest: [authMiddleware, loggingMiddleware],
  onBeforeResponse: [metricsMiddleware],
});

One important caveat with SolidStart’s middleware: it does NOT run during client-side navigation. Only on the initial server request. So don’t use it as your sole auth check - put auth logic in your server functions too. Next.js has the same nuance with proxy.ts, but it’s less obvious.

Deployment

Next.js works best on Vercel (unsurprisingly). Self-hosting has improved - the Build Adapters API is now stable, and Node.js/Docker deployments are better supported than before. But some optimizations are still Vercel-specific, and Turbopack remains Next.js-only - not the general-purpose bundler it was initially marketed as.

SolidStart deploys anywhere Nitro can target - and that’s a long list: Node, Deno, Bun, Cloudflare (Workers, Pages), Netlify, Vercel, AWS Lambda, Deno Deploy, and more. Switch targets with a single config line:

export default defineConfig({
  server: {
    preset: "cloudflare_pages", // or "netlify", "node", "deno", etc.
  },
});

No vendor lock-in. Vite is framework-agnostic with a massive ecosystem. This is the “primitives, not frameworks” philosophy applied to infrastructure.

Metaframework cheat sheet

ConcernNext.js 16SolidStart
RoutingFile-based, special files (page.tsx, layout.tsx, loading.tsx)File-based, file name = route, layouts via same-name convention
SSRDefault for Server Components, implicit mode switchingOn by default, explicit config for SSG/SPA
Server codeServer Components + Server Actions + Route HandlersServer functions ("use server") - one mechanism
Data fetchingasync Server Components, "use cache", SWR/TanStack for clientquery() + createAsync() - one model
Caching"use cache" + Cache Components (4 layers historically)query() with explicit cache keys
MutationsServer Actions via "use server" in functionsaction() with "use server"
API routesapp/api/*/route.tsroutes/api/*.ts
Middlewareproxy.ts (Node) + middleware.ts (Edge)createMiddleware (server-only)
Self-hostingStable Adapter APINitro presets (native)
OptimizationReact Compiler (auto-memoization)No compiler needed (signals)
BundlerTurbopack (Next.js-only)Vite (framework-agnostic)
DeploymentBest on Vercel, Adapter API for othersAnywhere via Nitro presets

Primitives, not frameworks

Before we get into the “why bother” section, it’s worth understanding the philosophy behind Solid. There’s a mantra in the Solid community: “Primitives, not frameworks.”

The idea is simple: instead of giving you a monolithic framework with opinions about every layer of the stack, Solid gives you small, composable, reactive building blocks that you combine however you need. To paraphrase Ryan Carniato’s approach: here are a few powerful concepts - learn them, combine them, build on top of them. That’s the whole thing.

This shows up everywhere in the ecosystem:

  • Solid Primitives - a community library of composable building blocks organized by domain (inputs, media, browser APIs, network, animation). Each one is tree-shakeable, SSR-safe, and individually useful.
  • SolidStart’s architecture - v2 replaced its custom server layer (Vinxi) with Vite 6’s Environment API, leveraging an existing ecosystem primitive rather than maintaining bespoke infrastructure.
  • The plugin model - features like image optimization are opt-in plugins, not baked into the framework. You compose what you need.

Contrast this with React’s trajectory. React 19 shipped with Actions, useActionState, useOptimistic, use(), Server Components, Server Actions. React 19.2 added <Activity> (for hiding UI while preserving state and unmounting effects) and useEffectEvent. The React Compiler hit v1.0, auto-memoizing your code at build time. And that’s before you add Next.js 16 on top with "use cache" directives, proxy.ts for server-side logic, Turbopack, and Build Adapters.

It’s a lot. The React Compiler is genuinely impressive engineering - it makes React faster without changing the fundamental model. But in a sense, it’s a patch on an architecture that other frameworks have moved past. Solid solves the performance problem at the architectural level via fine-grained reactivity, making a compiler unnecessary. Angular, Vue, and Svelte have all moved toward signals too. React is the outlier choosing to solve it with tooling rather than primitives. (Though it’s worth noting that React itself has moved to an independent foundation under the Linux Foundation - a governance change that may influence its future direction.)

Neither approach is “wrong.” But they lead to very different developer experiences.

Why bother? Performance and DX

So, that’s a lot of “this works differently.” Why go through the trouble?

Bundle size

Solid’s core clocks in at ~7KB min+gzip. React + ReactDOM sits at ~45KB min+gzip. That’s roughly a 6x difference at the framework level, and the gap compounds in real-world apps as you add routing, state management, and other dependencies. Less JavaScript shipped means faster load times. Simple math.

No virtual DOM overhead

React diffs a virtual tree on every state change, even with the Compiler auto-memoizing what it can. Solid compiles JSX to direct DOM operations. When a signal updates, only the specific DOM nodes that read that signal get touched. No diffing, no reconciliation, no wasted work. This is why Solid consistently ranks at or near the top of the JS Framework Benchmark, performing close to vanilla JavaScript.

The DX angle

Next.js has grown into a complex beast. Four layers of caching, three different caching models across three major versions (v14’s aggressive implicit caching, v15’s opt-out, v16’s "use cache" opt-in), the server/client boundary dance, the middleware-to-proxy.ts migration… the State of JS 2025 survey showed Next.js with the largest satisfaction drop of any meta-framework (from 68% to 55%), landing as both the 13th most-loved and 5th most-hated project - uniquely polarizing. A commonly cited complaint? “Too complex.” (paraphrased)

And then there was React2Shell (CVE-2025-55182) - a CVSS 10.0 critical RCE vulnerability in the React Server Components Flight protocol. A single malicious POST request to any server function endpoint could execute arbitrary code. Default create-next-app deployments were vulnerable out of the box. It was patched across all React 19.x lines (19.0.1, 19.1.2, and 19.2.1 - make sure you’re on the right patch for your minor version), and Next.js shipped corresponding fixes. But the architectural debate about the Flight protocol - and the attack surface of running component deserialization on the server - remains a valid concern when evaluating RSC-heavy architectures.

SolidStart, by contrast, is refreshingly lean. File-based routing, "use server" directives for server functions, query() for cached data fetching, action() for mutations, and Vite under the hood. It leans on the web platform and doesn’t try to reinvent every wheel. You write the reactive primitives we covered above, and the metaframework gets out of your way.

That said - SolidStart’s ecosystem is smaller. You won’t find the same breadth of third-party integrations, and the community, while active and growing (35K+ GitHub stars, ~1.5M weekly npm downloads), is not React-sized. It’s a tradeoff worth being honest about - however, in my personal experience, that hasn’t been a big deal. I’ve already shipped 5+ Solid-based projects of various complexity levels. I can’t say the ecosystem size was a huge problem.

Can LLMs help with the migration?

Short answer: yes, but trust and verify.

LLMs like Claude, ChatGPT, and tools like Cursor are genuinely useful for the mechanical parts of migration. They’ll convert useState to createSignal, swap className for class, strip out React.memo and useCallback (neither are needed in Solid), and handle the basic boilerplate. For a large codebase, that saves real time.

But here’s the catch: LLMs are trained on way more React code than Solid code. Their muscle memory defaults to React patterns, and the places where they fail are exactly the places where Solid differs most:

  1. Props destructuring - the #1 failure. Every LLM will write ({ name, onClick }) by default. In Solid, this kills reactivity. You need props.name or splitProps().
  2. Control flow - LLMs leave .map(), ternaries, and && in JSX instead of converting to <For>, <Show>, and <Switch>/<Match>. The JS patterns “work” but defeat Solid’s fine-grained updates.
  3. The “runs once” model - LLMs generate code that assumes the component function re-runs on state changes. It doesn’t in Solid.
  4. Async in effects - LLMs will happily write createEffect(async () => {...}), which breaks reactive tracking after the first await.

There’s a documented case of ChatGPT confidently telling a user that a bug in their <Show> usage was a Solid framework bug. It wasn’t. The LLM just didn’t understand Solid’s reactivity model.

Making it work

The good news is you can steer LLMs in the right direction:

  • SolidJS now ships an official llms.txt that you can feed to your AI tool as context. If you’re using Cursor, add it via @Docs.
  • solidjs-context-llms is a community-maintained, LLM-optimized documentation suite organized by domain (reactivity, routing, SSR, primitives).
  • Cursor users can grab SolidJS-specific .cursorrules files that enforce the right patterns.
  • Claude Code users can add Solid rules to their CLAUDE.md to prevent the common mistakes.

But the real safety net is eslint-plugin-solid. Run it after every AI-assisted conversion. It catches reactivity violations that LLMs introduce: destructured props, conditional logic outside JSX, incorrect imports. Think of it as the linter that keeps the AI honest.

My recommended workflow: convert one component at a time, review the output for the known failure patterns, and run the linter before moving on. LLMs get you 70-80% of the way there. The last 20% is where understanding the mental model matters, and that’s something you have to bring yourself.

Where to go from here

If you want to get your hands dirty:

Takeaway

Migrating from React to Solid isn’t a find-and-replace job. The JSX similarity is a double-edged sword - it makes the syntax feel familiar while hiding a fundamentally different execution model. But once the “runs once” mental model clicks, you’ll find that Solid’s approach is simpler and more predictable. And on a personal note, it makes my brain hurt less.

We’re also witnessing a convergence on fine-grained reactivity as the right model - Angular adopted signals, Vue has ref() and reactive(), Svelte 5 moved to runes. Solid has been doing this from day one. The primitives are mature, the metaframework is production-ready, and the philosophy of composable building blocks over monolithic abstractions means you’re not fighting the framework - you’re using it as intended.

Whether you’re moving away from Next.js for practical reasons, performance reasons, or just because you’re curious about what fine-grained reactivity feels like in practice - give SolidStart a shot. The learning curve is real but short, and the payoff is worth it.

Happy hacking!

Stay in the loop

New articles, talks, and the occasional deep cut — straight to your inbox.