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
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 :
Server receives request for /users/123
path matching: /users/:id โ params = { id: "123" }
Execute preload function โ returns SSRQuery[]
Direct backend call to UserModel.getUser("C", 123) (no HTTP!)
Inject result with QueryClient.setQueryData(["User", "getUser", "C", 123], result)
Send HTML + dehydratedState to client
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.
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