npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bgub/fig-tanstack-start

v0.1.2

Published

TanStack Start adapter for Fig

Readme

@bgub/fig-tanstack-start

The TanStack Start adapter for Fig. TanStack owns builds, requests, route loading, redirects, and server-function transport; Fig owns rendering, data resources, asset resources, and the single data store used by loaders and components.

pnpm add @bgub/fig-tanstack-start @bgub/fig-tanstack-router

Add the adapter to Vite:

import { tanstackStart } from "@bgub/fig-tanstack-start/plugin/vite";

const plugins = [tanstackStart()];

The plugin supplies the default client and server entries, including streamed Fig SSR, full-document hydration, and state-preserving Fig Fast Refresh. It currently uses TanStack's Solid target as a private compiler compatibility layer because plugin core has no custom framework target. The generator currently normalizes file-route constructor imports to its Solid package ID; the plugin maps that ID directly to Fig, and no Solid Router or Start adapter runtime is installed or bundled. TypeScript needs the corresponding compiler-only paths entry:

{
  "compilerOptions": {
    "paths": {
      "@tanstack/solid-router": ["./node_modules/@bgub/fig-tanstack-router"],
      "@tanstack/solid-start": ["./node_modules/@bgub/fig-tanstack-start"]
    }
  }
}

Response preload headers

The default server entry leaves response Link headers disabled because a CDN may cache and replay request-specific asset URLs. A custom server entry can opt in globally or provide a resource filter:

import { createFigStartHandler } from "@bgub/fig-tanstack-start/server";

const fetch = createFigStartHandler({
  preloadHeader: {
    filter: (resource) => resource.href.startsWith("/assets/"),
  },
});

export default { fetch };

The header contains assets discovered before Fig's document shell becomes ready. Assets discovered later through Suspense or Payload remain in the HTML stream and cannot be added after the response is created.

The Start mapping lets the generated registration footer carry middleware context types from a conventional src/start.ts into createServerFn.

Create the router with a root-neutral Fig store:

import { createStartDataContext } from "@bgub/fig-tanstack-start";
import {
  createRouter,
  createRootRouteWithContext,
} from "@bgub/fig-tanstack-router";

const startData = createStartDataContext();
const rootRoute = createRootRouteWithContext<typeof startData.context>()({
  component: Document,
});

export const router = createRouter({
  ...startData,
  routeTree: rootRoute,
});

The root document renders route-managed assets followed by Start's combined Fig data and TanStack script transport:

import { StartScripts } from "@bgub/fig-tanstack-start";
import { HeadContent, Outlet } from "@bgub/fig-tanstack-router";

function Document() {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <Outlet />
        <StartScripts />
      </body>
    </html>
  );
}

StartScripts establishes where the adapter writes the Fig store with Fig's value codec, inserts initial Payload responses, and renders TanStack's bootstrap scripts. The separate data block is intentional: TanStack freezes its hydration graph before Fig renders, while the adapter waits for the document render before snapshotting settled data first discovered in route components. Client router creation decodes the data before TanStack hydration can start route loaders; hydrateStart repeats that step idempotently as a fallback before hydrateRoot adopts the same client store. The first readData therefore hits the hydrated entry without re-running its loader, and invalidateData operates directly on the live root store.

CSS

Use TanStack Start's standard side-effect CSS imports and enable build-time inlining when desired:

import "./styles.css";
tanstackStart({
  server: { build: { inlineCss: true } },
});

HeadContent emits the server's inline style and adopts its contents when TanStack dehydrates an empty client placeholder, so full-document hydration keeps the page styled.

Request and function middleware

The package root also exports TanStack's createStart, createMiddleware, and createCsrfMiddleware. A conventional src/start.ts configures global request and server-function middleware:

import {
  createCsrfMiddleware,
  createMiddleware,
  createStart,
} from "@bgub/fig-tanstack-start";

const requestContext = createMiddleware({ type: "request" }).server(
  ({ request, next }) =>
    next({ context: { requestId: request.headers.get("x-request-id") } }),
);

export const startInstance = createStart(() => ({
  requestMiddleware: [
    requestContext,
    createCsrfMiddleware({
      filter: (context) => context.handlerType === "serverFn",
    }),
  ],
}));

Start's global async context remains request-local across interleaved SSR and server-function work. Redirects thrown by generated route loaders or beforeLoad use Router Core's normal server and client redirect handling.

Server functions

The package root exports TanStack's createServerFn. The Vite plugin compiles the handler into the server build and the browser call into an RPC request:

import { createServerFn } from "@bgub/fig-tanstack-start";

export const renameUser = createServerFn({ method: "POST" })
  .validator((input: { id: string; name: string }) => input)
  .handler(async ({ data }) => {
    await database.users.rename(data.id, data.name);
  });

An async event must capture the Fig store before its first await when it needs to refresh data afterward:

import { readDataStore } from "@bgub/fig";

const data = readDataStore();
await renameUser({ data: { id, name }, signal });
data.invalidateData(userResource, id);

Payload routes

Payload routes keep Payload-rendered component trees out of the client bundle while using the same Fig data store as ordinary route data. The smallest route is one colocated declaration:

// profile.payload.tsx
import { createPayloadComponent } from "@bgub/fig-dom";
import { serverPayload } from "@bgub/fig-tanstack-start/payload";
import { Profile } from "./Profile.server.tsx";

export const ProfilePage = createPayloadComponent<{ id: string }>({
  key: ["profile-payload"],
  load: serverPayload(Profile),
});

serverPayload accepts a component or render callback; it is the semantic boundary that the compiler extracts into a private server function. It may be async. The browser bundle keeps the component handle, cache, and RPC stub; it omits the server render and imports used only by it. Keep the Payload component declaration in a client-importable module such as .payload.tsx because the route imports that handle. The rendered component itself may live in a TanStack-protected .server.tsx module.

When the tree grows, the callback can import ordinary components. Those imports also stay out of the browser bundle when only the callback uses them. A filename does not cause a component to render through Payload—serverPayload does. Applications do not call createServerFn or renderPayloadResponse for Payload resources.

Mark the exceptional SSR-plus-hydration boundary with Isomorphic and an ordinary static component import. For example, a separate Payload component can contain the boundary:

import type { FigNode } from "@bgub/fig";
import { Isomorphic } from "@bgub/fig-tanstack-start/payload";
import { LikeButton } from "./LikeButton.tsx";

export function Profile({ id }: { id: string }): FigNode {
  return <Isomorphic component={LikeButton} userId={id} />;
}

The Vite plugin replaces only the component prop with an opaque Payload reference and generates its server/browser module resolver. The component remains an ordinary export and can render through Payload elsewhere without the boundary. component must be a named or default static import. Applications do not use a .client.tsx suffix, clientReference, createPayloadClientReferenceResolver, reference ids, or dynamic imports.

The route can start the load early, then render the component normally:

import { ensureRouteData } from "@bgub/fig-tanstack-router";
import { createFileRoute } from "@tanstack/solid-router";
import { ProfilePage } from "../profile.payload.tsx";

export const Route = createFileRoute("/profiles/$id")({
  loader: ({ context, params }) =>
    ensureRouteData(context, ProfilePage, { id: params.id }),
  component: ProfileRoute,
});

function ProfileRoute() {
  const { id } = Route.useParams();
  return <ProfilePage id={id} />;
}

When the route does not need the tree before commit, preload it and return void; Fig Suspense then owns the pending UI and stream:

import { Suspense } from "@bgub/fig";

export const Route = createFileRoute("/profiles/$id")({
  loader: ({ context, params }) => {
    context.data.preloadData(ProfilePage, { id: params.id });
  },
  component: () => (
    <Suspense fallback={<p>Streaming profile…</p>}>
      <ProfileRoute />
    </Suspense>
  ),
});

On SSR, Fig decodes and renders the root once, retains Payload-discovered asset resources on the rows that declared them, and embeds the response bytes for hydration. This includes Payload resources registered after the document shell starts. The document renderer emits each asset before its dependent HTML segment, including streamed Suspense holes. The browser adopts the embedded bytes without a second server-function call. Shell HTML streams while Suspense holes settle; TanStack starts full-document hydration after each complete initial Payload response is embedded in a keyed carrier. Client navigation and refresh use the same raw response path.

The Vite adapter follows ordinary component imports from each serverPayload render and compiles their static stylesheet imports into Payload asset dependencies. Import CSS normally; no manual assets(stylesheet(...)) wrapper or ?url import is needed. A Payload-rendered component's stylesheet is copied from the server build into the public client output. An Isomorphic component's hashed client CSS is attached through the generated manifest. Both use the existing Payload asset row and reveal gate and are emitted only when their component renders.

The demo-tanstack-start app exercises the adapter through Vite's production client and SSR builds: generated and split file routes, streamed SSR, Router dehydration, Fig-owned data serialization, full-document hydration, request-derived themes, view transitions, live data-resource invalidation, nested routes, and post and asset Payload trees all run through public adapter entries. The asset route embeds two independent Payload resources and hydrates an explicit Isomorphic component whose CSS and SVG are emitted through the production builds.