React
Build fully server-rendered React applications with complete type safety from your database to your components.
Build Configuration
Add React to your build by specifying the directory containing your React components:
1const manifest = await build({
2 reactDirectory: 'src/frontend'
3});Page Handler
Use handleReactPageRequest to render your components. Pass the component, its bundled index file, and optional props:
1// backend/server.ts
2import { handleReactPageRequest, asset } from '@absolutejs/absolute';
3import { Home } from '../frontend/pages/Home';
4
5new Elysia()
6 .get('/', () =>
7 handleReactPageRequest(Home, asset(manifest, 'HomeIndex'))
8 )
9 .get('/about', () =>
10 handleReactPageRequest(About, asset(manifest, 'AboutIndex'))
11 )Page Components
In AbsoluteJS, React components render the complete HTML document including html, head, and body tags. This gives you full control over meta tags, scripts, and page structure.
Props passed from your server are fully typed and available in your component:
1type HomeProps = {
2 user: User | null;
3 posts: Post[];
4};
5
6export const Home = ({ user, posts }: HomeProps) => (
7 <html lang="en">
8 <head>
9 <meta charSet="utf-8" />
10 <meta name="viewport" content="width=device-width, initial-scale=1" />
11 <title>Home | My App</title>
12 <link rel="stylesheet" href="/styles/global.css" />
13 </head>
14 <body>
15 <header>
16 <nav>
17 <a href="/">Home</a>
18 <a href="/about">About</a>
19 {user ? (
20 <span>Welcome, {user.name}</span>
21 ) : (
22 <a href="/auth/login">Login</a>
23 )}
24 </nav>
25 </header>
26 <main>
27 <h1>Latest Posts</h1>
28 <ul>
29 {posts.map((post) => (
30 <li key={post.id}>
31 <a href={`/posts/${post.slug}`}>{post.title}</a>
32 </li>
33 ))}
34 </ul>
35 </main>
36 </body>
37 </html>
38);End-to-End Type Safety
AbsoluteJS provides complete type safety from your database queries through your server handlers to your React components. TypeScript catches errors at compile time, not runtime.
Compile-Time Errors
Missing or incorrectly typed props are caught by TypeScript before your code runs.
Refactoring Safety
Rename a field in your database schema and TypeScript shows every place that needs updating.
Define your database schema and infer types directly from your table definitions using Drizzle:
1// db/schema.ts
2import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
3
4export const users = pgTable('users', {
5 id: text('id').primaryKey(),
6 name: text('name').notNull(),
7 email: text('email').notNull().unique()
8});
9
10export const notifications = pgTable('notifications', {
11 id: text('id').primaryKey(),
12 userId: text('user_id').notNull().references(() => users.id),
13 message: text('message').notNull(),
14 read: boolean('read').notNull().default(false),
15 createdAt: timestamp('created_at').notNull().defaultNow()
16});
17
18// types/databaseTypes.ts
19// Infer types directly from your table definitions
20export type User = typeof users.$inferSelect;
21export type NewUser = typeof users.$inferInsert;
22
23export type Notification = typeof notifications.$inferSelect;
24export type NewNotification = typeof notifications.$inferInsert;Use those types in your server handlers to ensure props match your components:
1// backend/server.ts
2import { User, Notification } from '../types/databaseTypes';
3import { Dashboard } from '../frontend/pages/Dashboard';
4
5type DashboardProps = {
6 user: User;
7 notifications: Notification[];
8 unreadCount: number;
9};
10
11new Elysia()
12 .get('/dashboard', async ({ cookie }) => {
13 const user = await getAuthenticatedUser(cookie);
14 const notifications = await getNotifications(user.id);
15
16 // Type error if props don't match DashboardProps!
17 return handleReactPageRequest(
18 Dashboard,
19 asset(manifest, 'DashboardIndex'),
20 {
21 user,
22 notifications,
23 unreadCount: notifications.filter(n => !n.read).length
24 }
25 );
26 })- Schema → Types: Drizzle infers types directly from your table definitions
- Types → Server: Your inferred types flow into route handlers and props
- Props → Component: React receives correctly typed props on both server and client
Hydration
AbsoluteJS automatically handles React hydration. Your component renders on the server, then React "hydrates" it on the client to make it interactive:
1// Client-side hydration happens automatically
2// Your component receives the same props on both server and client
3
4// 1. Server renders HTML with props
5// 2. Props are serialized to window.__INITIAL_PROPS__
6// 3. Client hydrates and receives identical props
7// 4. React attaches event handlers and makes page interactive
8
9export const Counter = ({ initialCount }: { initialCount: number }) => {
10 // useState works - hydration preserves server-rendered HTML
11 const [count, setCount] = useState(initialCount);
12
13 return (
14 <button onClick={() => setCount(c => c + 1)}>
15 Count: {count}
16 </button>
17 );
18};Index Files
AbsoluteJS automatically generates index files for client-side hydration. You never need to write these yourself — the build system creates them based on your page components.
By default, these generated files are deleted after bundling. To inspect them, enable preserveIntermediateFiles:
1// To inspect generated index files, enable preserveIntermediateFiles
2const manifest = await build({
3 reactDirectory: 'src/frontend',
4 options: {
5 preserveIntermediateFiles: true // Index files won't be deleted
6 }
7});Here's what a generated index file looks like:
1// Auto-generated index file (you don't write this!)
2// Generated at: src/frontend/indexes/HomeIndex.tsx
3
4import { hydrateRoot } from 'react-dom/client';
5import type { ComponentType } from 'react';
6import { Home } from '../pages/Home';
7
8type PropsOf<C> = C extends ComponentType<infer P> ? P : never;
9
10declare global {
11 interface Window {
12 __INITIAL_PROPS__: PropsOf<typeof Home>;
13 }
14}
15
16hydrateRoot(document, <Home {...window.__INITIAL_PROPS__} />);Streaming SSR
AbsoluteJS uses React's streaming SSR to send HTML progressively to the browser:
1// AbsoluteJS uses React's streaming SSR for optimal performance
2// Content is sent to the browser progressively
3
4// Benefits:
5// - First byte arrives faster (Time to First Byte)
6// - Content appears progressively (First Contentful Paint)
7// - Suspense boundaries stream independently
8
9export const Page = ({ data }: Props) => (
10 <html>
11 <body>
12 {/* This renders immediately */}
13 <Header />
14
15 {/* This streams when ready */}
16 <Suspense fallback={<Loading />}>
17 <AsyncContent data={data} />
18 </Suspense>
19 </body>
20 </html>
21);