Skip to content

Add TanStack Start experiment storefront - #3996

Draft
fredericoo wants to merge 3 commits into
fb-rename-examples-to-testbedfrom
fb-testbed-tanstack-start
Draft

Add TanStack Start experiment storefront#3996
fredericoo wants to merge 3 commits into
fb-rename-examples-to-testbedfrom
fb-testbed-tanstack-start

Conversation

@fredericoo

@fredericoo fredericoo commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #3995 (rename examples/ to experiments/). Review the diff against that branch.

TL;DR: adds a TanStack Start storefront to the experiments directory so we can see how Hydrogen's primitives fit a framework built around server functions and global request middleware. Feature set matches the React Router and Next.js templates (search, predictive search, sitemap, robots) plus the blog and account routes the sibling experiments carry.

What this changes

  • New experiments/tanstack-start/ (React 19, Vite 8, @tanstack/react-start, file-based routing).
  • One global request middleware (src/start.ts -> src/server/shopify-middleware.ts) owns the Hydrogen request lifecycle: request context, private Storefront client, encrypted customer session, handleShopifyRoutes short-circuit for /api/cart, /api/predictive-search, /account/*, /admin; context for server functions via next({ context }); session cookie commit and Hydrogen response headers afterwards. It knows nothing about individual routes.
  • Server functions are the only data path. src/server/storefront-fn.ts is a createServerFn builder that declares the middleware as a dependency, so handler context is typed and Start dedupes the already-executed global run. Loaders run on the server during SSR and as RPCs on client navigation; the private token never reaches the browser.
  • Not-found and Shopify URL redirects resolve server-side in one place (src/server/not-found.ts) and work identically on hard loads and client-side navigations, including query strings on redirect targets.
  • src/lib/search-params.ts gives the router a URLSearchParams-based parseSearch/stringifySearch so Hydrogen's string-based URL contract (incl. repeated-key filters) round-trips unchanged.
  • Sitemap and robots as server routes. Product JSON-LD. Analytics and consent via <ShopifyScripts>.
  • Root .oxlintrc.json: this project is linted (negation after the wholesale experiments/** ignore, routeTree.gen.ts excluded) with no rule exemptions. The five components ported from the React Router template used to sit behind its complexity exemption; both copies are refactored to pass complexity: 12 / max-depth: 3 outright and the React Router entries leave the exemption too (only CollectionBrowse.tsx keeps an assertions-only override for its fetcher.data cast).
  • dev:tanstack root script; rows in the root and experiments READMEs.

Developer impact

No @shopify/hydrogen changes, so no changeset. The one change outside the experiments directory is the React Router template: CartDrawer, CollectionCard, ProductCard, CollectionBrowse and routes/product.tsx are refactored to pass the complexity rule (same edits as the tanstack copies, no behaviour change; the portable e2e suite passes against it). The project README has a "Notes for the core SDK" section with friction worth a look:

  • CartData index signatures trip TanStack's serializable-return check (worked around with a type-only StripIndexSignatures).
  • parseCollectionParams output is not assignable to the Storefront API ProductFilter input (optional values, taxonomyMetafield without namespace).
  • No public getStandardRoute, so server functions rebuild storefront paths by hand to resolve redirects.
  • Storefront API errors arrive as HTTP 200 + errors; requireData surfaces them so a throttled API does not read as "not found".

UX impact

New storefront at experiments/tanstack-start, same design as the other experiments (core/tokens.css).

Out of scope

  • Nitro. It was in the plan, but its dev proxy rewrites the request host to its internal worker without x-forwarded-*, so Hydrogen saw an http origin over dev:https and Customer Account login failed. Current TanStack Start hosting docs treat Nitro as one optional adapter, so this uses Start's own dev server and srvx for start (the documented Nitro-free Node path).
  • E2E coverage (no sibling framework experiment has it).
  • Sitemap pagination beyond 250 products/collections.

Risk

  • Verified against the hydrogen-preview store in a browser: server-fn-only navigation (no *.myshopify.com requests from the browser), multi-value filters keep both repeated keys, after cursor dropped on filter change, variant options stay strings in the URL, add to cart opens the drawer, client-side navigation to a missing product or unknown path renders the 404 page, client-side navigation to /collections/hydrogen and /collections/x/products/y redirects, predictive search returns results, consent banner injected. / is cacheable; /account and /api/cart are private, no-store. build + start (srvx --prod) serve the same pages.
  • Anything requiring dev:https (Customer Account login end to end) was verified up to the 303 into Shopify OAuth, not through a real login.

How to Test

  1. pnpm install && pnpm run experiments:secrets:decrypt
  2. pnpm dev:tanstack and open the printed URL.
  3. Browse to a collection, toggle two values of one filter and change the sort; the URL should carry both filter keys and the grid should update without a full reload.
  4. Open a product, change an option; the URL updates in place and the price/availability follow.
  5. Add to cart; the drawer opens with the line. /cart shows the same line.
  6. Type in the header search; suggestions appear. Submit to land on /search?q=....
  7. Visit /collections/hydrogen by clicking a link you paste into the console (document.body.appendChild(Object.assign(document.createElement("a"), {href: "/collections/hydrogen", textContent: "x"}))) - it should redirect to /collections/backcountry without a page reload.
  8. /sitemap.xml, /robots.txt, /blogs/news, /account.
  9. For Customer Account: pnpm --filter @shopify/hydrogen-experiment-tanstack-start dev:https, open https://local.tryhydrogen.dev:5173/account, click "Log in".

@fredericoo fredericoo self-assigned this Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

little scope creep but this was refactored to respect oxlint's complexity and max lines so we can stop ejecting out of those rules

faced this when writing the tanstack app

Comment thread .oxlintrc.json
Comment on lines -143 to -149
"templates/react-router/app/components/CartDrawer.tsx",
"templates/react-router/app/components/CollectionBrowse.tsx",
"templates/react-router/app/components/CollectionCard.tsx",
"templates/react-router/app/components/ProductCard.tsx",
"templates/react-router/app/routes/collection.tsx",
"templates/react-router/app/routes/product.tsx",
"templates/react-router/app/routes/search.tsx",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that's what we were able to remove from the exceptions

New experiments/tanstack-start project matching the React Router and Next.js
template feature set: home, collections, collection (filters, sort, load
more), product (URL-synced variants, add to cart, Shop Pay, streamed related
products), cart page + drawer, search + predictive search, blog + article,
Customer Account page, catch-all with Shopify URL redirects, sitemap and
robots server routes, analytics and consent scripts.

Architecture:
- One global request middleware (src/start.ts -> shopify-middleware.ts)
  owns the Hydrogen request lifecycle: request context, private Storefront
  client, encrypted customer session, handleShopifyRoutes short-circuit,
  context for server functions, session cookie commit + response headers.
- Server functions are the only data path; storefrontFn declares the
  middleware as a dependency so handler context is typed without a global
  registry. Loaders run on the server during SSR and as RPCs on client nav.
- Not-found and Shopify redirects resolve server-side in one place and work
  on hard loads and client-side navigations alike.
- URLSearchParams-based router search encoding so Hydrogen's string-based URL
  contract (incl. repeated-key filters) round-trips unchanged.
- Production served by srvx from the plain Vite build; Nitro was dropped
  because its dev proxy loses the HTTPS origin needed for Customer Account.

Lint now covers this project (negation after the wholesale experiments ignore);
README records SDK and TanStack friction found along the way.

Assisted-By: devx/79ee620c-af83-4d1e-8173-901af0bff678
- Related products promise resolves to [] on failure so a rejected deferred
  promise can never become an unhandled rejection when getProduct throws.
- useLoadMore resets isLoading when the result set changes mid-flight.
- Storefront API errors (HTTP 200 + errors) are surfaced via requireData
  instead of being read as not-found and triggering a redirect lookup.
- Product JSON-LD (plan parity with the template PDPs).
- /account opts out of intent preloading (each hover would hit the Customer
  Account API).
- Lint: ported components get complexity/max-depth parity with the React
  Router template only, with a why-comment; the two type assertions are
  replaced with a narrowing parser and satisfies.
- README: scoped build/start/typecheck commands, tsr warning note; drop dead
  Nitro entries from .gitignore.

Assisted-By: devx/79ee620c-af83-4d1e-8173-901af0bff678
Five functions shared verbatim between templates/react-router and
experiments/tanstack-start sat behind complexity/max-depth exemptions:
CartLineItem (43), the VariantOptions value renderer (20), describeFilter
(19), ProductCard (16) and CollectionCard (15). Both copies now pass the
default limits and the exemptions are gone: the tanstack override block is
deleted and the React Router entries leave the shared complexity group.
React Router's CollectionBrowse.tsx moves to the assertions-only override
because it still casts fetcher data; collection.tsx and search.tsx never
violated anything and lose their exemption outright.

The splits follow real seams rather than line counts: describeLine flattens
the nullable merchandise/product chain once; cardImage makes explicit that
the product fallback image never claims dimensions; ProductCardBadge encodes
sold-out-beats-sale as guard returns instead of two ternaries that had to
agree; OptionValueControl owns the Link-vs-button decision that was
duplicated across swatch and pill branches.

Both copies stay byte-identical outside the pre-existing Link/useLocation
framework adaptations.

Assisted-By: devx/79ee620c-af83-4d1e-8173-901af0bff678
@fredericoo
fredericoo force-pushed the fb-rename-examples-to-testbed branch from 7609ce5 to 647d59e Compare September 9, 2026 15:38
@fredericoo
fredericoo force-pushed the fb-testbed-tanstack-start branch from 163669f to cd2c420 Compare September 9, 2026 15:39
@fredericoo fredericoo changed the title Add TanStack Start testbed storefront Add TanStack Start experiment storefront Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant