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:
// absolute.config.ts
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
reactDirectory: 'src/frontend'
});#Page Handler
Use handleReactPageRequest to render your components. Pass the component, its bundled index file, and optional props:
Framework handlers are non-streaming by default. Add { collectStreamingSlots: true } as the 4th argument to enable out-of-order slot streaming forSuspenseSlot and StreamSlot.
// backend/server.ts
import { asset } from '@absolutejs/absolute';
import { handleReactPageRequest } from '@absolutejs/absolute/react';
new Elysia()
.get('/', () =>
handleReactPageRequest({ Page: Home, index: asset(manifest, 'HomeIndex') })
)
.get('/about', () =>
handleReactPageRequest({ Page: About, index: asset(manifest, 'AboutIndex') })
)#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:
type HomeProps = {
user: User | null;
posts: Post[];
};
export const Home = ({ user, posts }: HomeProps) => (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Home | My App</title>
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
{user ? (
<span>Welcome, {user.name}</span>
) : (
<a href="/auth/login">Login</a>
)}
</nav>
</header>
<main>
<h1>Latest Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</main>
</body>
</html>
);#Hydration
AbsoluteJS automatically handles React hydration. Your component renders on the server, then React "hydrates" it on the client to make it interactive:
// Client-side hydration happens automatically
// Your component receives the same props on both server and client
// 1. Server renders HTML with props
// 2. Props are serialized to window.__INITIAL_PROPS__
// 3. Client hydrates and receives identical props
// 4. React attaches event handlers and makes page interactive
export const Counter = ({ initialCount }: { initialCount: number }) => {
// useState works: hydration preserves server-rendered HTML
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
};#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:
// To inspect generated index files, enable preserveIntermediateFiles
// absolute.config.ts
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
reactDirectory: 'src/frontend',
options: {
preserveIntermediateFiles: true // Index files won't be deleted
}
});Here's what a generated index file looks like:
// Auto-generated index file (you don't write this!)
// Generated at: src/frontend/indexes/HomeIndex.tsx
import { hydrateRoot } from 'react-dom/client';
import type { ComponentType } from 'react';
import { Home } from '../pages/Home';
type PropsOf<C> = C extends ComponentType<infer P> ? P : never;
declare global {
interface Window {
__INITIAL_PROPS__: PropsOf<typeof Home>;
}
}
hydrateRoot(document, <Home {...window.__INITIAL_PROPS__} />);#Streaming SSR
AbsoluteJS routes React streaming throughSuspenseSlot and can send HTML progressively when the page handler opts in:
// AbsoluteJS uses React's streaming SSR for optimal performance
// Content is sent to the browser progressively
// Benefits:
// - First byte arrives faster (Time to First Byte)
// - Content appears progressively (First Contentful Paint)
// - Suspense boundaries stream independently
export const Page = ({ data }: Props) => (
<html>
<body>
{/* This renders immediately */}
<Header />
{/* This streams when ready */}
<Suspense fallback={<Loading />}>
<AsyncContent data={data} />
</Suspense>
</body>
</html>
);