Skip to content

fix: clean up v2 templates list#78

Open
nerdchanii wants to merge 2 commits into
solidjs-community:mainfrom
nerdchanii:main
Open

fix: clean up v2 templates list#78
nerdchanii wants to merge 2 commits into
solidjs-community:mainfrom
nerdchanii:main

Conversation

@nerdchanii

Copy link
Copy Markdown

Purpose

This PR cleans up the v2 templates list to align with the solidjs/templates repository.

Context

The template list is currently hardcoded in constants.ts.
but templates are fetched from the solidjs/templates repo.

This mismatch can cause issues when templates are added/removed in the templates repo but not reflected here.

Ref: solidjs/templates

Changes

  • Removed non-v2 templates (hackernews, notes, v1 with-solidbase)
  • Sorted the template list alphabetically for better maintainability

Before/After

  • hackernews
  • notes
  • with-solidbase (v1 version)

Sorted order:
with-authwith-authjswith-drizzlewith-mdxwith-prismawith-solid-styledwith-solidbasewith-strict-cspwith-tailwindcss
with-tanstack-routerwith-trpcwith-unocsswith-vitest

Files Changed

  • packages/create/src/utils/constants.ts

Appendix: Future Automation Ideas

To keep the template list in sync with solidjs/templates, we could consider:

  1. GitHub Actions Approach:
    • Set up a workflow that watches solidjs/templates repo changes
    • Automatically updates constants.ts when v2 templates are added/removed
    • Creates a PR for review
  2. Runtime Computation:
    • Use GitHub API to fetch available templates dynamically
    • Cache the result to avoid API rate limits
    • Fallback to hardcoded list if API fails
      Both approaches would eliminate manual maintenance and prevent sync issues. Thoughts?

p.s. Thank you for your efforts and for maintaining this project.

"with-solidbase",
"with-strict-csp",
"with-tailwindcss",
"with-tanstack-router",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i actually don't think the with-tanstack-router one works at the moment

@nerdchanii nerdchanii Jul 20, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i actually don't think the with-tanstack-router one works at the moment

Thank you for the comment.

I tested the template and confirmed the issue as you mentioned (CSR works fine, but an error occurs on the SSR path).

Would it be better to only include fully functional templates here? I think this comes down to the Solid community's UX goals and philosophy, so I'd love to hear your thoughts.

I originally opened this PR to sync the template list between the v2 templates list and this Repository.

What would be the best way forward?
Personally, if removing this line is the recommended approach, I think closing this PR and opening a fresh one might be healthier for context hygiene (especially considering future AI workflow).

So, I think we can choose two options:

  1. Comment out or remove with-tanstack-router from the list
  2. Update the with-tanstack-router template

P.S. Although option 2 should probably be treated as a separate issue from this PR, I've investigated the root cause and put together a fix. I'll leave those details in a separate comment below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just remove the with-tanstack-router from t he list would be fine

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR to remove with-tanstack-router from the list. I left the deep-dive analysis and potential fixes in the comment below as well

@nerdchanii

nerdchanii commented Jul 20, 2026

Copy link
Copy Markdown
Author

@brenelz

i actually don't think the with-tanstack-router one works at the moment

Honest sequence of events: my first reaction was "fair enough, I'll just delete it from the list — one line, done." But then I got curious. One "wait, why doesn't this work?" led to another. That single comment sent me down a delightful rabbit hole, and I came back with the full picture (plus two working fix approaches).


TL;DR (Summary for maintainers)

  1. The with-tanstack-router template now fails SSR(HTTP 500).
    This looks like a regression from the v2 rewrite of @solidjs/start, which silently dropped the routerLoad API the template was relying on.

  2. The root cause: the template calls createHandler with 3 args (the v1 API),
    but v2 only accepts 2, so the 3rd arg is silently ignored. As a result, SSR crashes because the router's internal store is never initialized.

  • Two ways to fix it:
    • Approach A — Fix the template only (no solid-start changes).
    • Approach B — Restore routerLoad in solid-start (backward-compatible).

      If we don't go with Approach B, the docs should be updated to drop routerLoad from the createHandler signature in solid-docs.

  1. There's a small issue independent of the above: TanStack Router officially recommends a fresh router instance per request, but the template uses a module-level singleton.
    So, Whichever approach we pick, we should also switch the template to a per-request router instance to avoid state leaking across concurrent requests.

So, the decisions needed:

(a) For the v2 template list(Choose one)

  • Opt1: Remove with-tanstack-router
  • Opt2: Keep it with a note pending the upstream fix.

(b) For the SSR fix itself(Choose one)

  • Opt1: Approach A (template-only) + update solid-docs to drop routerLoad from createHandler
  • Opt2: Approach B (restore routerLoad in solid-start) — no doc change needed.

(c) Independent of (b)

Should we also roll in the singleton-to-per-request router fix (as recommended by TanStack Router) described in the "Recommended addition" section?

Two fix approaches (both verified locally)

Both approaches produce:

  • curl /200 with fully rendered HTML.
  • curl /about200, zero SSR errors.

Approach A — Fix the template only (no solid-start changes)

Leverage the 2nd options parameter, which accepts an async (context) => HandlerOptions function that is awaited before rendering.

Move routerLoad logic into this options function.

Diff (entry-server.tsx):

-import { createHandler, FetchEvent, StartServer } from "@solidjs/start/server";
+import { createHandler, StartServer } from "@solidjs/start/server";
 import { createMemoryHistory } from "@tanstack/solid-router";
 import { router } from "./router";

-const routerLoad = async (event: FetchEvent) => {
-  const url = new URL(event.request.url);
-  const path = url.href.replace(url.origin, "");
-  router.update({
-    history: createMemoryHistory({ initialEntries: [path] })
-  });
-  await router.load();
-};
-
 export default createHandler(
   () => <StartServer document={...} />,
-  undefined,
-  routerLoad
+  async (context) => {
+    const url = new URL(context.request.url);
+    const path = url.href.replace(url.origin, "");
+    router.update({
+      history: createMemoryHistory({ initialEntries: [path] })
+    });
+    await router.load();
+    return {};
+  }
 );

Approach B — Restore routerLoad in solid-start

Add routerLoad back as an optional 3rd parameter to createHandler, wrapping createPageEvent to call await routerLoad(e) before rendering. Fully backward-compatible.

Diff (handler.ts):

 export function createHandler(
   fn: (context: PageEvent) => JSX.Element,
   options: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>) = {},
-  ): H3 {
-  return createBaseHandler(createPageEvent, fn, options);
+  routerLoad?: (event: FetchEvent) => Promise<void>,
+) {
+  if (routerLoad) {
+    const _createPageEvent = createPageEvent;
+    const customCreatePageEvent = async (e: FetchEvent) => {
+      const pageEvent = await _createPageEvent(e);
+      await routerLoad(e);
+      return pageEvent;
+    };
+    return createBaseHandler(customCreatePageEvent, fn, options);
+  }
+  return createBaseHandler(createPageEvent, fn, options);
 }

Recommended addition

fresh router instance per request (applies to both A and B)

Both approaches above "work" — they were each verified with a single curl request. But the template's router.tsx exports a module-level singleton, and both fixes call router.update() + router.load() on that same shared instance per request. TanStack Router's official SSR guidance does the opposite — a fresh router per request — and for good reason: concurrency isolation.

  • Official docs pass createRouter as a function (setup-ssr)
  • The start-basic-nitro example exports a getRouter() factory, not a singleton (source)
  • TanStack's createRequestHandler calls createRouter() once per request (createRequestHandler.ts)

Since @solidjs/start v2 (Nitro/h3) serves concurrent requests on a single event loop with no per-request serialization, two requests sharing one router instance can leak state into each other. The yield window is inside load() (where loadMatches awaits Promise.all of the route loaders), and onReady re-reads the shared store before committing — so a second request can overwrite stores.pendingMatches while the first is suspended, then the first request renders the wrong route's data.

Caveat on reproduction: this race requires async route loaders to open the yield window. The current template only has static routes with no loaders, so a single curl won't reproduce it — which is exactly why it slipped through my initial verification. In any real app that does data loading (fetch / DB / createServerFn), the structure is unsafe even though the demo passes.

The proper fixrouter.tsx keeps createRouter() as a factory and drops the singleton export, and the server entry creates a fresh instance per request:

- export const router = createRouter();
+ // keep createRouter() as the factory; drop the singleton export
  // entry-server.tsx (Approach A, hardened)
- import { router } from "./router";
+ import { createRouter } from "./router";

  async (context) => {
    const path = new URL(context.request.url).href.replace(origin, "");
-   router.update({ history: createMemoryHistory({ initialEntries: [path] }) });
-   await router.load();
+   const router = createRouter();                                    // fresh per request
+   router.update({ history: createMemoryHistory({ initialEntries: [path] }) });
+   await router.load();
    return {};
  }

(Client side stays a single instance — entry-client.tsx just calls createRouter() once.)

I'd suggest rolling this router.tsx change into whichever fix PR we go with. Happy to verify the per-request version under concurrent load (e.g. wrk / ab with an async loader) if useful.

Questions for maintainers

  1. Should with-tanstack-router be removed from the v2 list, or kept with a note pending the upstream fix?
  2. Between Approach A (template-only) and Approach B (solid-start API restoration), which direction would you prefer?
  3. Regardless of A vs B — should we also roll in the singleton → per-request router fix (the "Recommended addition" above) in the same PR?

🔍 Debugging notes — full deep dive

Root cause

The template's entry-server.tsx calls createHandler(fn, undefined, routerLoad) with 3 arguments

— the 2nd arg (undefined) and 3rd arg (routerLoad) (source), where routerLoad initializes TanStack's memory history and calls router.load() before rendering.

However, @solidjs/start v2's createHandler only accepts 2 arguments (fn, options) — the 3rd argument is not used by the current implementation (source).

During SSR, without router.update() + router.load(), the router's internal __store is never initialized, causing useStore() to crash with:

TypeError: Cannot read properties of undefined (reading 'state')
  ...

CSR is unaffected because the client-side createBrowserHistory() auto-initializes the router. TanStack router itself is not buggy — standalone Node.js tests confirm that calling router.update() before rendering produces valid SSR output.

Why SSR crashes but CSR works

The router.update() / router.load() calls are the hook that tells TanStack router to initialize its internal store (__store). Without them during SSR:

  1. useRouterState() reads from the uninitialized router store
  2. useStore(router, 'state') receives undefined instead of the state object
  3. Accessing undefined.state throws TypeError: Cannot read properties of undefined (reading 'state')

On the client, createBrowserHistory() is called during router setup, which triggers the initialization automatically — so CSR works fine.

Key code path in createBaseHandler

const context = await createPageEvent(event);          // L65

const resolvedOptions =                                // L67-68
  typeof options === "function"                        //    <-- if options is async fn,
    ? await options(context)                           //    <-- awaited HERE, before render
    : { ...options };

// ... mode check ...

const html = renderToString(() => {                    // L73
  (sharedConfig.context as any).event = context;
  return fn(context);                                  // L75 <-- actual rendering
}, resolvedOptions);

The await options(context) at L69-71 is the key insight for Approach A: whatever we put in this async function runs before fn(context) (the render). This is where we can safely call router.update() + router.load().


Race condition — singleton router under concurrent requests

This is the deep dive behind the "Recommended addition" section above. The window is not between update() and load() (that stretch is synchronous, no yield) — it's inside load():

Request A (/about):
  update({ history: '/about' })                         // sync
  await load()
    → beforeLoad() sync: stores.location = '/about',
                       stores.pendingMatches = [/about]  // writes shared store
    → await loadMatches() → await Promise.all(loaders)   // YIELD (load-matches.ts L1030)
Request B (/):
  update({ history: '/' })                              // sync
  await load()
    → beforeLoad() sync: stores.location = '/',
                       stores.pendingMatches = [/]       // OVERWRITES shared store
    → await loadMatches() → ...                          // YIELD
A resumes:
  → onReady: pendingMatches = stores.pendingMatches.get()  // now [/]  (router.ts L2522)
  → stores.setMatches([/])                                  // commits / data (L2569)
  → renderToString() → renders / data for the /about request  // BUG

Key facts from the TanStack source:

  • update() is fully synchronous and mutates this.history / this.latestLocation (router-core/router.ts).
  • beforeLoad() is synchronous and writes to the shared this.stores (router.ts L2454-2455).
  • The actual await yield is in loadMatchesPromise.all of the route loaders (load-matches.ts L1030).
  • onReady re-reads the shared stores.pendingMatches before committing (router.ts L2522), which is where another request's overwrite gets picked up.

h3 doesn't serialize requests, and AsyncLocalStorage isolation covers the framework's request context but not an app-exported module singleton — so the two requests can cross-contaminate.


Fix verification details

Approach A — Fix the template (entry-server.tsx rewrite)

Verification procedure:

  1. Scaffolded a new project from the with-tanstack-router template
  2. Applied the diff above to src/entry-server.tsx
  3. Ran npm run dev (uses @solidjs/start@2.0.0-beta.10)
  4. Tested with curl while dev server was running:
$ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/
200

$ curl -s http://localhost:3000/ | grep -o '<title>[^<]*</title>\|Clicks: [0-9]*\|<a[^>]*>[^<]*</a>'
<title>SolidStart</title>
<a href="/">Index</a>
<a href="/about">About</a>
Clicks: 0

$ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/about
200

$ curl -s http://localhost:3000/about | grep -o 'About'
About
  1. No SSR error messages in the dev server console. Browser navigation (including client-side route transitions) worked correctly.

Approach B — Restore routerLoad in solid-start (handler.ts patch)

Verification procedure:

  1. Built solid-start locally from the modified handler.ts (pnpm run build in packages/start)
  2. Linked the local build to the project via pnpm link
  3. Used the original, unmodified template entry-server.tsx (3-arg call with routerLoad)
  4. Ran npm run dev with the linked local solid-start build
  5. Tested:
$ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/
200

$ curl -s http://localhost:3000/ | grep -o '<title>[^<]*</title>\|Clicks: [0-9]*\|<a[^>]*>[^<]*</a>'
<title>SolidStart</title>
<a href="/">Index</a>
<a href="/about">About</a>
Clicks: 0

$ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/about
200
  1. No SSR error messages in the console. Browser navigation worked correctly.

Note: TanStack router itself is not buggy. I verified by running the router-core in a standalone Node.js script: calling router.update({ history: createMemoryHistory(...) }) followed by router.load() before rendering produces valid SSR output. The issue is purely in the wiring between the template and @solidjs/start.


Full regression history

The routerLoad regression — from introduction to removal to the present broken state:

Date PR / Commit What happened Evidence
2025-03-03 #1840 Added routerLoad as 4th param to createBaseHandler (then 3rd to createHandler) specifically for TanStack router SSR support Patch: routerLoad param added
2025-11-07 #1942 Full v2 rewrite of handler.ts (vinxi → h3, major restructuring). routerLoad removed entirely — no replacement API provided. Patch: routerLoad removed, createHandler reduced to 2 args (see handler.ts diff)
2026-03-17 #254 "Reintegrate SolidStart v1" — copied v1's entry-server.tsx (which uses the 3-arg createHandler with routerLoad) into the v2 template without modification Template file
2026-05-06 solid-docs docs team create-handler.mdx still documents routerLoad as a valid API parameter
Current main @solidjs/start main All current releases ship 2-arg createHandler. The SSR bug persists.
2026-07-18 #2206 Stream output fix (cancellation-safe `ReadableStream`). Unrelated to this issue (different layer: stream transport vs. router initialization). N/A

Notable: TanStack/router#3521 tracks SSR support for the Solid driver — related context but distinct from the routerLoad wiring issue.

nerdchanii added a commit to nerdchanii/solid-cli that referenced this pull request Jul 20, 2026
Removed with-tanstack-router as it currently has an SSR issue in v2.
The detailed deep-dive investigation and fix options have been documented in PR solidjs-community#78 for future reference.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants