Rendering Pages
The Page Object
Every Inertia response is a "page object" -- a JSON structure that tells the client which component to render and what data to pass it:
{
component: 'auth/login', // Maps to assets/js/pages/auth/login.jsx
url: '/login', // Current URL
version: '1.0', // Asset version (for cache busting)
props: { // Data passed to the component
passkeyChallenge: null,
flash: {},
errors: {}
},
deferredProps: {}, // Props loaded after initial render
mergeProps: [], // Props that merge instead of replace
scrollRegions: [], // Scroll position metadata
encryptHistory: false, // Whether to encrypt history state
clearHistory: false // Whether to clear history
}Using responseType: 'inertia'
The most common pattern for GET routes. The action returns an object with page (component name) and optionally props:
Simple Page (No Props)
// api/controllers/home/view-home.js
module.exports = {
exits: {
success: { responseType: 'inertia' }
},
fn: async function () {
return { page: 'index' }
}
}Page with Props
// api/controllers/auth/view-login.js
module.exports = {
exits: {
success: { responseType: 'inertia' }
},
fn: async function () {
const passkeyChallenge = this.req.session.passkeyChallenge || null
if (passkeyChallenge) delete this.req.session.passkeyChallenge
return {
page: 'auth/login',
props: { passkeyChallenge }
}
}
}Page with Data Fetching
// api/controllers/setting/view-team.js
module.exports = {
inputs: {
teamId: { type: 'number', required: true }
},
exits: {
success: { responseType: 'inertia' },
notFound: { responseType: 'notFound' }
},
fn: async function ({ teamId }) {
var team = await Team.findOne({ id: teamId })
if (!team) throw 'notFound'
var memberships = await Membership.find({ team: team.id }).populate(
'member'
)
var pendingInvites = await TeamInvite.find({
team: team.id,
status: 'pending'
})
return {
page: 'settings/team',
props: {
team,
memberships,
pendingInvites
}
}
}
}How Rendering Works Internally
When sails.inertia.render(req, res, data) is called:
- Resolves shared props -- Merges global shared props (from
sails.inertia.share()) with the action's page-specific props - Handles partial reloads -- If the request has
X-Inertia-Partial-DataorX-Inertia-Partial-Exceptheaders, only the requested props are resolved - Resolves deferred props -- Identifies
DeferPropinstances and separates them from immediate props - Resolves merge/once props -- Processes
MergeProp,OnceProp, andAlwaysPropinstances - Builds the page object -- Combines component name, URL, version, and resolved props
- Returns the response:
- First visit (no
X-Inertiaheader): Rendersviews/app.ejswith the page object indata-page - Subsequent visits (has
X-Inertiaheader): Returns the page object as JSON withX-Inertia: trueheader
- First visit (no
The page Property
The page string maps directly to a file path under assets/js/pages/:
page value |
File path |
|---|---|
'index' |
assets/js/pages/index.jsx |
'auth/login' |
assets/js/pages/auth/login.jsx |
'settings/profile' |
assets/js/pages/settings/profile.jsx |
'dashboard/view-dashboard' |
assets/js/pages/dashboard/view-dashboard.jsx |
Overriding the Root View
By default, Inertia renders using views/app.ejs. You can override this per-request:
// Use a different root view for this request
sails.inertia.setRootView('auth') // Uses views/auth.ejs insteadThis is useful for having different HTML shells (e.g., a minimal layout for auth pages).
Locals (Root Template Data)
Actions can return a locals object to pass data to the root EJS template (views/app.ejs). Locals are for the HTML shell -- <title>, <meta>, Open Graph tags -- not for your page components (use props for that).
return {
page: 'courses/show',
props: { course },
locals: {
title: course.title,
description: course.description,
ogImage: course.thumbnailUrl
}
}See locals.md for the full API, precedence rules, and real-world examples.
Rendering on Non-GET Routes
The inertia response type is almost always used with GET routes. For POST/PATCH/PUT/DELETE routes, you typically redirect after processing (see redirects-and-responses.md).
However, there are edge cases where a non-GET action renders a page:
// api/controllers/setting/delete-profile.js
module.exports = {
exits: {
success: { responseType: 'inertiaRedirect' },
hasTeamMembers: { responseType: 'inertia' } // Show modal instead of redirecting
},
fn: async function () {
// If user has team members, show a page instead of deleting
if (hasMembers) {
throw {
hasTeamMembers: {
page: 'settings/profile',
props: { showTransferModal: true }
}
}
}
// ... proceed with deletion
}
}