@storesynk/react
v0.1.0-beta.13
Published
Framework-agnostic React bindings for Storesynk — server-render Shopify data inside Storesynk tags from any classic-SSR React stack (TanStack Start, Remix, Vite SSR), adopted flash-free by the client runtime.
Maintainers
Readme
@storesynk/react
Framework-agnostic React bindings for Storesynk — server-render Shopify data
inside Storesynk tags from any classic-SSR React stack (TanStack Start,
Remix / React Router, vanilla Vite SSR), adopted flash-free by the client
runtime. The sibling of @storesynk/next, minus the RSC coupling.
How it differs from @storesynk/next
Next's wrappers are async Server Components. In classic SSR a component renders on the server and again at hydration, so it can't be async — instead the async half (fetch + template stringify + core's server transform) runs in your framework's data layer and returns a serializable prepared object that feeds a synchronous component:
loader / createServerFn component (server + hydration)
renderProduct(<Template/>, {handle}) ──▶ <StoresynkProduct prepared={...} />
= fetch + transform + payload = one dangerouslySetInnerHTMLBoth renders paint the same prepared bytes, so hydration can never mismatch, React never reconciles the engine-mutated subtree, and the client runtime adopts the embedded payloads with zero refetch.
Setup
npm i @storesynk/react
npx @storesynk/react init # scaffolds storesynk.config.tsAdd the credentials to your env (public Storefront token only — never Admin):
SHOPIFY_STORE_DOMAIN=your-shop.myshopify.com
SHOPIFY_STOREFRONT_ACCESS_TOKEN=...Import the config for its side effect at the top of your root module
(TanStack Start: the router/root route module; Remix: app/root.tsx). On
runtimes without process.env (Cloudflare Workers without nodejs_compat),
pass domain/token to configureStoresynk directly.
TanStack Start
// src/templates/product.tsx — static host elements only (no state/handlers)
export const ProductTemplate = () => (
<>
<h1><show-title></show-title></h1>
<show-image main widths="400,800"></show-image>
<change-option></change-option>
<show-price></show-price>
<add-to-cart>Add to cart</add-to-cart>
</>
);
// src/routes/products.$handle.tsx
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { getWebRequest } from '@tanstack/react-start/server';
import { StoresynkProduct, StoresynkStore } from '@storesynk/react';
import { createRequestScope, renderProduct, renderStore } from '@storesynk/react/server';
import { ProductTemplate } from '../templates/product';
const getPage = createServerFn({ method: 'GET' })
.validator((handle: string) => handle)
.handler(async ({ data: handle }) => {
// `request` carries everything per-request: the buyer's locale cookie, the
// URL's variant selection / listing filters, the JSON-LD canonical. Explicit
// options (`locale`, `state`, `selectedOptions`) always win over it.
const opts = { request: getWebRequest(), fresh: true, scope: createRequestScope() };
return {
store: await renderStore(opts),
product: await renderProduct(<ProductTemplate />, { handle, ...opts }),
};
});
export const Route = createFileRoute('/products/$handle')({
loader: ({ params }) => getPage({ data: params.handle }),
component: () => {
const { store, product } = Route.useLoaderData();
return (
<StoresynkStore prepared={store}>
<StoresynkProduct prepared={product} />
</StoresynkStore>
);
},
});On document loads (SSR, reloads, shared links)
getWebRequest()is the page request, so filtered collection URLs and variant links server-render exactly. On client-side navigations a server fn sees its own RPC request instead — the helpers then fall back to the default view and the client engine applies the URL state itself, which is the right division of labor anyway.
Remix / React Router
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const opts = { request, fresh: true, scope: createRequestScope() };
return {
store: await renderStore(opts),
product: await renderProduct(<ProductTemplate />, { handle: params.handle!, ...opts }),
};
};
export default function ProductPage() {
const { store, product } = useLoaderData<typeof loader>();
return (
<StoresynkStore prepared={store}>
<StoresynkProduct prepared={product} />
</StoresynkStore>
);
}What's included
| @storesynk/react/server (loaders) | @storesynk/react (components) |
| --------------------------------------- | -------------------------------- |
| renderStore | <StoresynkStore> |
| renderProduct | <StoresynkProduct> |
| renderList | <StoresynkList> |
| renderCollection (URL filter/sort/page state derives from request; parseListingParams to override) | <StoresynkCollection> |
| renderLocalePicker | <StoresynkLocalePicker> |
| renderProductJsonLd | <StoresynkProductJsonLd> |
| productSeo (head-API-neutral) | — |
| getRequestLocale / getRequestCustomer (cookie/Request) | — |
| checkGate (returns the decision — map it to your router's redirect/notFound) | — |
| getProduct, getShop, getProductHandles, … (raw fetch helpers) | <StoresynkStatic> (no-data markup: cart drawer, client-only templates) |
Templates handed to the render helpers must be static host elements (the same tags as a plain HTML page — no client components, no state, no event handlers) or a raw HTML string. Everything the transform can't fill server-side (cart, predictive search, money/file/list metafields) hydrates client-side exactly as on a static page.
Not included (Next-specific, no framework-neutral equivalent): the
server-owned cart (HttpOnly cookie id via server actions) — the default
client cart (localStorage id, direct Storefront API) works everywhere.
Notes
- Cloudflare Workers / edge: everything is
fetch-based and Node-API-free; pass credentials viaconfigureStoresynkifprocess.envis absent. - Caching: fetches are module-cached per process (build-time economy).
Dynamic routes pass
fresh: trueplus a sharedcreateRequestScope()so one request's helpers dedupe without leaking across requests. - React 18: works at runtime; the bundled JSX typings for the Storesynk
tags require React 19's
@types/react(React 18 consumers can keep their ownJSX.IntrinsicElementsdeclarations). <StoresynkStatic>pullsreact-dom/serverinto the client bundle (it re-stringifies at hydration); the prepared-render components don't.
