Server-side rendering (SSR) allows you to deliver pre-rendered HTML to users for faster initial page loads and better SEO. Cerebral provides robust support for SSR through the UniversalApp
API.
When a user visits your application, the typical SSR flow follows these steps:
www.example.com
On your server, create a fresh UniversalApp
instance for each request:
import { UniversalApp } from 'cerebral'
import main from '../client/main' // Your main module
app.get('/', (req, res) => {
// Create a new app instance for each request to prevent state leakage
const cerebral = UniversalApp(main)
// Continue with rendering...
})
Fetch any necessary data and update the app state:
// Run a sequence to fetch data and update state
await cerebral.runSequence(
[
// Fetch data based on the current route
({ http, props }) => {
return http.get('/api/data').then((response) => ({ data: response.data }))
},
// Update state with the fetched data
({ store, props }) => {
store.set(state`pageData`, props.data)
}
],
{
// Initial props for the sequence
route: req.path
}
)
Use your view library’s server rendering method:
// With React
import { renderToString } from 'react-dom/server'
import { Container } from '@cerebral/react'
import App from '../client/components/App'
// Render the app to a string
const appHtml = renderToString(
<Container app={cerebral}>
<App />
</Container>
)
Create a script tag containing the state changes to hydrate the client app:
// Generate script tag with serialized state
const stateScript = cerebral.getScript()
Combine everything into a complete HTML response:
res.send(`<!DOCTYPE html>
<html>
<head>
<title>My Cerebral App</title>
${stateScript}
</head>
<body>
<div id="root">${appHtml}</div>
<script src="/static/bundle.js"></script>
</body>
</html>`)
On the client, use a regular App
instance that will automatically pick up the state:
// client.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from 'cerebral'
import { Container } from '@cerebral/react'
import main from './main'
import AppComponent from './components/App'
// Standard app - automatically picks up window.CEREBRAL_STATE
const app = App(main)
// Hydrate the app
const root = createRoot(document.getElementById('root'))
root.render(
<Container app={app}>
<AppComponent />
</Container>
)
This example shows a complete Express server setup for SSR:
import express from 'express'
import path from 'path'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { UniversalApp, state } from 'cerebral'
import { Container } from '@cerebral/react'
import main from '../client/main'
import App from '../client/components/App'
const server = express()
const PORT = process.env.PORT || 3000
// Serve static files
server.use('/static', express.static(path.resolve(__dirname, '../dist')))
// Handle all routes
server.get('*', async (req, res) => {
try {
// 1. Create fresh app instance
const cerebral = UniversalApp(main)
// 2. Run initialization sequence based on route
await cerebral.runSequence(
[
({ store, props }) => {
// Store current URL
store.set(state`currentPage`, props.url)
// We could fetch data based on the route here
// For example, if url is /users/123, fetch user with id 123
if (props.url.startsWith('/users/')) {
const userId = props.url.split('/')[2]
return { userId }
}
},
// Conditionally fetch data based on previous action's return value
({ http, props }) => {
if (props.userId) {
return http
.get(`/api/users/${props.userId}`)
.then((response) => ({ user: response.data }))
}
},
// Store fetched data
({ store, props }) => {
if (props.user) {
store.set(state`currentUser`, props.user)
}
}
],
{
url: req.path
}
)
// 3. Render the app
const appHtml = renderToString(
<Container app={cerebral}>
<App />
</Container>
)
// 4. Get state script for hydration
const stateScript = cerebral.getScript()
// 5. Send the response
res.send(`<!DOCTYPE html>
<html>
<head>
<title>Cerebral SSR Example</title>
${stateScript}
</head>
<body>
<div id="root">${appHtml}</div>
<script src="/static/bundle.js"></script>
</body>
</html>`)
} catch (error) {
console.error('SSR Error:', error)
res.status(500).send('Server Error')
}
})
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
When implementing SSR with routing, you need to synchronize the server-side state with the current route:
await cerebral.runSequence(
[
({ store, props }) => {
// Initialize routing state based on the request URL
store.set(state`router.currentPath`, props.url)
store.set(state`router.query`, props.query)
// Extract route parameters if needed
const params = extractParamsFromUrl(props.url)
store.set(state`router.params`, params)
// Determine which data to load based on the route
return {
route: determineRoute(props.url),
params
}
},
// Load data based on the route
({ http, props }) => {
switch (props.route) {
case 'home':
return http
.get('/api/home-data')
.then((response) => ({ pageData: response.data }))
case 'user':
return http
.get(`/api/users/${props.params.id}`)
.then((response) => ({ userData: response.data }))
default:
return { pageData: null }
}
},
// Store loaded data
({ store, props }) => {
if (props.pageData) {
store.set(state`pageData`, props.pageData)
}
if (props.userData) {
store.set(state`currentUser`, props.userData)
}
}
],
{
url: req.path,
query: req.query
}
)
Your client-side router should be configured to read from and update this same state structure to ensure consistency between server and client rendering.
To optimize your SSR implementation:
Handle APIs that only exist in the browser:
function safelyUseWindow({ props }) {
// Check if we're in a browser environment
if (typeof window !== 'undefined') {
window.localStorage.setItem('key', props.value)
}
}
Create environment-specific providers:
const localStorageProvider =
typeof window !== 'undefined'
? {
get(key) {
return window.localStorage.getItem(key)
},
set(key, value) {
window.localStorage.setItem(key, value)
}
}
: {
get() {
return null
},
set() {}
}
// In your module
export default {
providers: {
storage: localStorageProvider
}
}
Server-side rendering with Cerebral gives you the benefits of fast initial page loads and SEO while maintaining the power and organization of a Cerebral application. By using the UniversalApp
API, you can seamlessly share state between server and client.
For more detailed API information, refer to the UniversalApp API documentation.