Skip to main content
Learn how to preload data on the server and pass it to the client using Sonamuโ€™s registerSSR.

Data Preloading Overview

registerSSR

Per-route preload config Direct backend call

SSRQuery

Type-safe queries Model/method specification

Auto Injection

Auto inject to QueryClient Hydration handling

No HTTP

No network overhead Fast response

How to Use registerSSR

Basic Structure

// api/src/application/sonamu.ts
import { registerSSR } from "sonamu";

registerSSR({
  path: "/users/:id",
  preload: (params) => [
    {
      modelName: "UserModel",
      methodName: "getUser",
      params: ["C", parseInt(params.id)],
      serviceKey: ["User", "getUser"],
    },
  ],
});
Process:
  1. Server receives request for /users/123
  2. path matching: /users/:id โ†’ params = { id: "123" }
  3. Execute preload function โ†’ returns SSRQuery[]
  4. Direct backend call to UserModel.getUser("C", 123) (no HTTP!)
  5. Inject result with QueryClient.setQueryData(["User", "getUser", "C", 123], result)
  6. Send HTML + dehydratedState to client
  7. Client hydrates and immediately uses data

SSRQuery Type

type SSRQuery = {
  modelName: string; // "UserModel" - Backend model class name
  methodName: string; // "getUser" - Model method name
  params: unknown[]; // ["C", 123] - Method parameters (excluding Context)
  serviceKey: [string, string]; // ["User", "getUser"] - React Query queryKey prefix
};
Important: params follows the backend methodโ€™s parameter order (excluding Context).

Practical Examples

Single Data Loading

Preload user information on user detail page.
// api/src/application/sonamu.ts
registerSSR({
  path: "/users/:id",
  preload: (params) => [
    {
      modelName: "UserModel",
      methodName: "getUser",
      params: ["C", parseInt(params.id)], // subset, id order
      serviceKey: ["User", "getUser"],
    },
  ],
});
// web/src/routes/users/$id.tsx
import { createFileRoute } from "@tanstack/react-router";
import { UserService } from "@/services/services.generated";

export const Route = createFileRoute("/users/$id")({
  component: UserPage,
});

function UserPage() {
  const { id } = Route.useParams();

  // Automatically uses data preloaded from server
  // isLoading: false (data already exists)
  const { data } = UserService.useUser("C", parseInt(id));

  return (
    <div>
      <h1>{data?.user.username}</h1>
      <p>{data?.user.email}</p>
      <p>Bio: {data?.user.bio}</p>
    </div>
  );
}

Multiple Data Simultaneous Loading

Preload both post and comments on post detail page.
// api/src/application/sonamu.ts
registerSSR({
  path: "/posts/:id",
  preload: (params) => [
    // Post data
    {
      modelName: "PostModel",
      methodName: "getPost",
      params: ["C", parseInt(params.id)],
      serviceKey: ["Post", "getPost"],
    },
    // Comment list
    {
      modelName: "CommentModel",
      methodName: "getCommentsByPost",
      params: [parseInt(params.id)],
      serviceKey: ["Comment", "getCommentsByPost"],
    },
  ],
});
// web/src/routes/posts/$id.tsx
import { createFileRoute } from "@tanstack/react-router";
import { PostService, CommentService } from "@/services/services.generated";

export const Route = createFileRoute("/posts/$id")({
  component: PostPage,
});

function PostPage() {
  const { id } = Route.useParams();

  // Both data are preloaded
  const { data: post } = PostService.usePost("C", parseInt(id));
  const { data: comments } = CommentService.useCommentsByPost(parseInt(id));

  return (
    <div>
      <article>
        <h1>{post?.post.title}</h1>
        <p>{post?.post.content}</p>
      </article>

      <section>
        <h2>Comments ({comments?.comments.length})</h2>
        {comments?.comments.map((comment) => (
          <div key={comment.id}>{comment.content}</div>
        ))}
      </section>
    </div>
  );
}

Parameter Processing

You can process URL parameters before use.
// api/src/application/sonamu.ts
registerSSR({
  path: "/categories/:slug/posts",
  preload: (params) => {
    // Logic to convert slug to id
    const categoryId = getCategoryIdBySlug(params.slug);

    return [
      {
        modelName: "CategoryModel",
        methodName: "getCategory",
        params: [categoryId],
        serviceKey: ["Category", "getCategory"],
      },
      {
        modelName: "PostModel",
        methodName: "getPostsByCategory",
        params: [categoryId, { page: 1, pageSize: 20 }],
        serviceKey: ["Post", "getPostsByCategory"],
      },
    ];
  },
});

Conditional Preloading

Load different data based on conditions.
// api/src/application/sonamu.ts
registerSSR({
  path: "/dashboard/:tab",
  preload: (params) => {
    const queries: SSRQuery[] = [
      // Common data: User info
      {
        modelName: "UserModel",
        methodName: "getCurrentUser",
        params: [],
        serviceKey: ["User", "getCurrentUser"],
      },
    ];

    // Load additional data based on tab
    if (params.tab === "posts") {
      queries.push({
        modelName: "PostModel",
        methodName: "getMyPosts",
        params: [{ page: 1, pageSize: 20 }],
        serviceKey: ["Post", "getMyPosts"],
      });
    } else if (params.tab === "settings") {
      queries.push({
        modelName: "SettingsModel",
        methodName: "getUserSettings",
        params: [],
        serviceKey: ["Settings", "getUserSettings"],
      });
    }

    return queries;
  },
});

Query Key Matching

For preloaded data to match the clientโ€™s useQuery, queryKey must match exactly.

Correct Matching

// Server: registerSSR
{
  modelName: "UserModel",
  methodName: "getUser",
  params: ["C", 123],
  serviceKey: ["User", "getUser"],  // Stored as ["User", "getUser", "C", 123]
}

// Client: useQuery
UserService.useUser("C", 123);  // queryKey: ["User", "getUser", "C", 123]
// โœ… Match successful!

Incorrect Matching

// Server: Preloaded with Subset "C"
{
  params: ["C", 123],
  serviceKey: ["User", "getUser"],
}

// Client: Requesting Subset "A"
UserService.useUser("A", 123);  // queryKey: ["User", "getUser", "A", 123]
// โŒ Match failed - Client makes new API call

SSRRoute Options

disableHydrate

Disable Hydration and render new on client.
registerSSR({
  path: "/admin/report",
  preload: (params) => [
    /* ... */
  ],
  disableHydrate: true, // Disable Hydration
});
Use cases:
  • When server/client rendering results may differ
  • When real-time data is important
  • Resolving Hydration mismatch

cacheControl

Set Cache-Control header for SSR response.
registerSSR({
  path: "/posts/:id",
  preload: (params) => [
    /* ... */
  ],
  cacheControl: {
    maxAge: 3600, // 1 hour cache
    sMaxAge: 7200, // 2 hour CDN cache
    staleWhileRevalidate: 86400, // Serve stale content for 1 day
  },
});

Internal Operation Principle

1. Server Rendering Process

// sonamu/src/ssr/renderer.ts (simplified)
export async function renderSSR(url: string, route: SSRRoute, params: Record<string, string>) {
  // 1. Execute preload
  const preloadConfig = route.preload ? route.preload(params) : [];
  const preloadedData: PreloadedData[] = [];

  // 2. Execute each SSRQuery
  for (const { modelName, methodName, params: apiParams, serviceKey } of preloadConfig) {
    const api = Sonamu.syncer.apis.find(
      (a) => a.modelName === modelName && a.methodName === methodName,
    );

    // 3. Direct backend API call (no HTTP!)
    const result = await Sonamu.invokeApiForSSR(api, apiParams);

    // 4. Save PreloadedData
    preloadedData.push({
      queryKey: [...serviceKey, ...apiParams],
      data: result,
    });
  }

  // 5. Call entry-server.generated.tsx's render()
  const { html, dehydratedState } = await render(url, preloadedData);

  // 6. Inject data into HTML
  const ssrDataScript = `<script>window.__SONAMU_SSR__ = ${JSON.stringify(dehydratedState)};</script>`;

  return html.replace("</body>", `${ssrDataScript}\n</body>`);
}

2. Data Injection in entry-server

// entry-server.generated.tsx
export async function render(url: string, preloadedData: PreloadedData[] = []) {
  const queryClient = new QueryClient();

  // Inject PreloadedData into QueryClient
  for (const { queryKey, data } of preloadedData) {
    queryClient.setQueryData(queryKey, data);
  }

  // Dehydrate (serialize)
  const dehydratedState = dehydrate(queryClient);

  // React rendering
  const appHtml = renderToString(<RouterProvider router={router} />);

  return { html: appHtml, dehydratedState };
}

3. Client Hydration

// entry-client.tsx
// Restore data from window.__SONAMU_SSR__
const dehydratedState = window.__SONAMU_SSR__;

if (dehydratedState) {
  // Hydrate into QueryClient
  hydrate(queryClient, dehydratedState);
}

// React Hydration
ReactDOM.hydrateRoot(document, <RouterProvider router={router} />);

Error Handling

Handling Preload Failures

registerSSR({
  path: "/posts/:id",
  preload: (params) => {
    try {
      return [
        {
          modelName: "PostModel",
          methodName: "getPost",
          params: ["C", parseInt(params.id)],
          serviceKey: ["Post", "getPost"],
        },
      ];
    } catch (error) {
      // Parameter parsing failure etc.
      console.error("Preload error:", error);
      return [];
    }
  },
});
Server log:
Failed to preload PostModel.getPost: Post not found
Individual query failure doesnโ€™t stop the entire SSR. Only failed data is reloaded on client.

Performance Optimization

1. Use Subsets

Load only needed fields to reduce transfer size.
// โŒ All fields (slow)
params: ["C", userId]; // All fields

// โœ… Only needed fields (fast)
params: ["A", userId]; // Only id, username, email

2. Parallel Loading

Execute multiple queries simultaneously (automatically parallelized).
preload: (params) => [
  // These queries execute in parallel
  { modelName: "UserModel", methodName: "getUser", ... },
  { modelName: "PostModel", methodName: "getPosts", ... },
  { modelName: "CommentModel", methodName: "getComments", ... },
]

3. Conditional Loading

Selectively load only necessary data.
preload: (params) => {
  const queries = [];

  // Base data
  queries.push({ modelName: "PageModel", methodName: "getPage", ... });

  // Authenticated users only
  if (hasAuth(params)) {
    queries.push({ modelName: "UserModel", methodName: "getProfile", ... });
  }

  return queries;
}

Cautions

Cautions when using registerSSR: 1. Match queryKey exactly: Server and client queryKey must be identical 2. Careful with params order: Must follow backend method parameter order exactly 3. Exclude Context: params doesnโ€™t include Context 4. Type conversion caution: Type conversion like parseInt(params.id) needed 5. Errors are logged: Individual query failure doesnโ€™t stop entire SSR

Next Steps

SSR Setup

SSR basic structure

Hydration Strategies

Hydration optimization

Cache Control

Caching strategies

Subset System

Data optimization