Frontend
Data fetching
A page loads its data in one place: handle. That is the same loader whether the page is rendered on the server for the first paint or resolved in the browser on a client navigation, so there is no separate server codebase to keep in sync.
#One loader, both sides
The principle
Universal apps usually split data loading into "server" and "client" paths that drift apart. If one loader runs in both places, that drift is impossible: the page fetches its data the same way wherever it renders.
In Stone.js
A page's handle is an event handler: it reads the event, resolves services, returns data. On SSR it runs server-side and the result is serialised into the page; on a client navigation it runs in the browser. Its return is the render's data.
import { Page, IPage, ReactIncomingEvent } from '@stone-js/use-react'
@Page('/tasks/:id')
export class TaskPage implements IPage<ReactIncomingEvent> {
// The loader: runs before render. On SSR it runs on the server; on a client
// navigation it runs in the browser. Same code, either way.
async handle (event: ReactIncomingEvent, { tasks }: { tasks: TaskService }) {
return tasks.find(event.get('id'))
}
render ({ data }: { data: Task }) {
return <h1>{data.title}</h1>
}
}import { definePage } from '@stone-js/use-react'
const TaskPage = ({ tasks }) => ({
handle: (event) => tasks.find(event.get('id')),
render: ({ data }) => <h1>{data.title}</h1>
})
export const pages = [definePage(TaskPage, { path: '/tasks/:id' }, true)]#Reading data
render receives data directly; deeper components read it with useData(), so you rarely thread it through props.
import { useData } from '@stone-js/use-react'
export function TaskMeta () {
const task = useData<Task>()
return <small>Created {task?.createdAt}</small>
}#Server-only loading
For work that must never run in the browser (secrets, a direct database call), a server loader keeps it server-side and hands the client only the result. Use it when handle would otherwise pull server-only code into the browser bundle.
import { defineServerLoader } from '@stone-js/use-react'
export const loadReport = defineServerLoader(async ({ event, container }) => {
return container.make('db').query('report', event.get('id')) // server only
})#Loading states
On the first paint (SSR/SSG) the data is already there, no spinner, no layout shift. On a client navigation, the loader runs before the new page shows; the current page stays put until it resolves, so there is no flash of empty UI. For slow loaders, render pending UI with React's ordinary tools (Suspense, a transition), the data layer does not fight them.