Add TanStack Start experiment storefront - #3996
Draft
fredericoo wants to merge 3 commits into
Draft
Conversation
fredericoo
commented
Sep 8, 2026
Contributor
Author
There was a problem hiding this comment.
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
fredericoo
commented
Sep 8, 2026
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", |
Contributor
Author
There was a problem hiding this comment.
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
force-pushed
the
fb-rename-examples-to-testbed
branch
from
September 9, 2026 15:38
7609ce5 to
647d59e
Compare
fredericoo
force-pushed
the
fb-testbed-tanstack-start
branch
from
September 9, 2026 15:39
163669f to
cd2c420
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3995 (rename
examples/toexperiments/). 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
experiments/tanstack-start/(React 19, Vite 8,@tanstack/react-start, file-based routing).src/start.ts->src/server/shopify-middleware.ts) owns the Hydrogen request lifecycle: request context, private Storefront client, encrypted customer session,handleShopifyRoutesshort-circuit for/api/cart,/api/predictive-search,/account/*,/admin; context for server functions vianext({ context }); session cookie commit and Hydrogen response headers afterwards. It knows nothing about individual routes.src/server/storefront-fn.tsis acreateServerFnbuilder that declares the middleware as a dependency, so handlercontextis 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.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.tsgives the router aURLSearchParams-basedparseSearch/stringifySearchso Hydrogen's string-based URL contract (incl. repeated-key filters) round-trips unchanged.<ShopifyScripts>..oxlintrc.json: this project is linted (negation after the wholesaleexperiments/**ignore,routeTree.gen.tsexcluded) 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 passcomplexity: 12/max-depth: 3outright and the React Router entries leave the exemption too (onlyCollectionBrowse.tsxkeeps an assertions-only override for itsfetcher.datacast).dev:tanstackroot script; rows in the root and experiments READMEs.Developer impact
No
@shopify/hydrogenchanges, so no changeset. The one change outside the experiments directory is the React Router template:CartDrawer,CollectionCard,ProductCard,CollectionBrowseandroutes/product.tsxare 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:CartDataindex signatures trip TanStack's serializable-return check (worked around with a type-onlyStripIndexSignatures).parseCollectionParamsoutput is not assignable to the Storefront APIProductFilterinput (optionalvalues,taxonomyMetafieldwithoutnamespace).getStandardRoute, so server functions rebuild storefront paths by hand to resolve redirects.errors;requireDatasurfaces 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
x-forwarded-*, so Hydrogen saw anhttporigin overdev:httpsand Customer Account login failed. Current TanStack Start hosting docs treat Nitro as one optional adapter, so this uses Start's own dev server andsrvxforstart(the documented Nitro-free Node path).Risk
*.myshopify.comrequests from the browser), multi-value filters keep both repeated keys,aftercursor 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/hydrogenand/collections/x/products/yredirects, predictive search returns results, consent banner injected./is cacheable;/accountand/api/cartareprivate, no-store.build+start(srvx --prod) serve the same pages.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
pnpm install && pnpm run experiments:secrets:decryptpnpm dev:tanstackand open the printed URL./cartshows the same line./search?q=..../collections/hydrogenby 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/backcountrywithout a page reload./sitemap.xml,/robots.txt,/blogs/news,/account.pnpm --filter @shopify/hydrogen-experiment-tanstack-start dev:https, openhttps://local.tryhydrogen.dev:5173/account, click "Log in".