This documentation introduces how to manually install and configure Inertia for a combo project.
Before we begin, we need to choose the frontend framework to use. Here we'll use React, but the process is similar for other Inertia-compatible frameworks, like Vue or Svelte.
Combo's project generator - combo_new, already includes all of this scaffolding and are the fastest way to get started with Combo and Inertia.
Compatibility
Inertia.js >= 2.0.0
Installation
Generating a project using Vite
When using Inertia, it's best to use it in conjunction with modern assets build tools like Vite. To get started quickly, let's create a project from vite template provided by combo_new:
$ mix combo_new vite my_app
Server-side setup
Installing dependencies
Add :combo_inertia to the list of dependencies in mix.exs:
def deps do
[
{:combo_inertia, "<requirement>"}
]
endSetting up necessary modules
This package includes a few modules:
Combo.Inertia.Plug- the plug for detecting Inertia requests and preparing the connection accordingly.Combo.Inertia.Conn- the%Plug.Conn{}helpers for rendering Inertia responses.Combo.Inertia.HTML- the HTML components and helpers for building Inertia views.
First, add Combo.Inertia.Plug into the browser pipeline:
# lib/my_app/web/router.ex
defmodule MyApp.Web.Router do
use MyApp.Web, :router
pipeline :browser do
# ...
+ plug Combo.Inertia.Plug
end
endThen:
- import
Combo.Inertia.Conninto the controller helper. - import
Combo.Inertia.HTMLinto the html helper.
# lib/my_app/web.ex
defmodule MyApp.Web do
# ...
def controller do
quote do
# ...
+ import Combo.Inertia.Conn
end
end
# ...
defp html_helpers do
quote do
# ...
+ import Combo.Inertia.HTML
# ...
end
end
# ...
endModifying the root layout
- add
data-ssrattribute to the<html>tag, which is supplied to client-side code to identify the current rendering mode. - replace the
<title>tag with the<.inertia_title>component, which is used to keep the title in sync with client-side code. - add the
<.inertia_head>component.
# lib/my_app/web/layouts/root.html.ceex
<!DOCTYPE html>
- <html lang="en">
+ <html lang="en" data-ssr={@inertia_ssr}>
<head>
<!-- ... -->
- <title>
- {if title = assigns[:page_title], do: "#{title} ยท MyApp", else: "MyApp"}
- </title>
+ <.inertia_title>
+ {if title = assigns[:page_title], do: "#{title} - MyApp", else: "MyApp"}
+ </.inertia_title>
+ <.inertia_head content={@inertia_head} />
- <.vite_assets names={["src/css/app.css", "src/js/app.js"]} />
+ <.vite_react_refresh />
+ <.vite_assets names={["src/js/app.jsx"]} />
</head>
<!-- ... -->You may noticed that we add
<.vite_react_refresh />component before the<.vite_assets />component. It's provided bycombo_vitefor enabling fast refresh in development, and only for React.
Adding configuration
If you'd like to add configuration, these configuration options are available.
config :my_app, MyApp.Web.Endpoint,
inertia: [
# Configures the asset versioning strategy.
#
# Available values:
# * `:auto` - Automatically determines version in following order:
# 1. check manifest file generated by Vite, and hash it if present
# 2. check manifest file generated by `combo.static.digest`, and hash it if present
# 3. Falls back to `"not-detected"` if no manifest found
#
# * a string - Uses a fixed version string
#
# * a {module, fun, args} tuple - Calls the specified function to generate version string
#
# Defaults to `:auto`
assets_version: :auto,
# Instruct the client side whether to encrypt the page object in the window
# history state.
# Defaults to `false`.
encrypt_history: false,
# Enable automatic conversion of prop keys from snake case to camel case.
# Defaults to `false`.
camelize_props: false,
# Enable server-side rendering for page responses (requires some additional setup,
# see instructions below).
# Defaults to `false`.
ssr: false,
# Whether to raise an exception when server-side rendering fails.
# Defaults to `true`.
raise_on_ssr_failure: true
]Client-side setup
Configuring Vite for React
Add @vitejs/plugin-react:
$ cd assets
$ npm install -D --install-links @vitejs/plugin-react
Edit assets/vite.config.js:
import { defineConfig } from "vite"
import combo from "vite-plugin-combo"
+ import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [
combo({
- input: ["src/css/app.css", "src/js/app.js"],
+ input: ["src/js/app.jsx"],
staticDir: "../priv/static",
}),
+ react(),
],
})Installing React and Inertia adapter
$ cd assets
$ npm install -S --install-links @inertiajs/react react react-dom
Creating the Inertia app
Next, rename app.js to app.jsx and update it to create your Inertia app:
// assets/src/js/app.jsx
import "vite/modulepreload-polyfill"
import "@fontsource-variable/instrument-sans"
import "../css/app.css"
import { createInertiaApp } from "@inertiajs/react"
import { createRoot } from "react-dom/client"
createInertiaApp({
resolve: (name) => {
const page = `./pages/${name}.jsx`
const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
return pages[page]
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})The resolve callback tells Inertia how to load a page component. It receives a page name as string, and returns a page component module. By default we recommend eager loading your components, which will result in a single JavaScript bundle. However, if you'd like to lazy-load your components, you can modify the resolve callback like this:
{
// ...
resolve: (name) => {
const page = `./pages/${name}.jsx`
const pages = import.meta.glob("./pages/**/*.jsx") // remove the {eager: true} option
return pages[page]() // add parentheses at the end
}
// ...
}See the code splitting documentation of Inertia for more information.
The setup callback receives everything necessary to initialize the client-side framework, including the root Inertia App component.
The above code assumes your pages live in the assets/src/js/pages directory and have a default export with page component, like this:
// assets/js/src/pages/Home.jsx
export default Home({ msg }) {
return <p>This is the home page. {msg}</p>
}Setting up CSRF protection
Combo.Inertia sets the CSRF token to CSRF-TOKEN cookie, and Combo expects to receive the CSRF token via the X-CSRF-TOKEN header
But, Axios, the HTTP library that Inertia uses under the hood, uses the following CSRF related config by default:
axios.defaults.xsrfCookieName = "XSRF-TOKEN"
axios.defaults.xsrfHeaderName = "X-XSRF-TOKEN"To make them work together, we should setup Axios:
// assets/src/js/app.jsx
import "vite/modulepreload-polyfill"
import "@fontsource-variable/instrument-sans"
import "../css/app.css"
+ import axios from "axios"
import { createInertiaApp } from "@inertiajs/react"
import { createRoot } from "react-dom/client"
+ axios.defaults.xsrfCookieName = "CSRF-TOKEN"
+ axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN"
createInertiaApp({
resolve: (name) => {
const page = `./pages/${name}.jsx`
const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
return pages[page]
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})Setting up SSR (optional)
Inertia comes with with server-side rendering (SSR) support.
The steps for enabling SSR similar to other backend frameworks, but instead of running a separate Node.js server process to render HTML,
Combo.Inertiaspins up a pool of Node.js process workers to handle SSR calls and manages the state of those node processes from your Elixir process tree. This is mostly just an implementation detail that you don't need to be concerned about, but we'll highlight how ourssr.jsscript differs from the Inertia docs.
To run Combo and a Node.js process pool with 1 process, you need at least 512MiB of memory. Otherwise, the machine may experience out-of-memory (OOM) errors or severe slowness.
Client-side setup
Adding the SSR entrypoint
Create a Node.js module that exports a render function to perform the actual server-side rendering of pages. Let's name it ssr.jsx.
// assets/src/js/ssr.jsx
import { createInertiaApp } from "@inertiajs/react"
import ReactDOMServer from "react-dom/server"
export function render(page) {
return createInertiaApp({
page,
render: ReactDOMServer.renderToString,
resolve: (name) => {
const page = `./pages/${name}.jsx`
const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
return pages[page]
},
setup: ({ App, props }) => <App {...props} />,
})
}This is similar to the server entry-point documented here, except we are simply exporting a function called
render, instead of starting a Node.js server process.
Configuring Vite for the SSR entrypoint
Configure vite to build assets/src/js/ssr.jsx, and put the bundled ssr.js into priv/ssr.
// assets/vite.config.js
import { defineConfig } from "vite"
import combo from "vite-plugin-combo"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [
combo({
input: ["src/js/app.jsx"],
staticDir: "../priv/static",
+ ssrInput: ["src/js/ssr.jsx"],
+ ssrOutDir: "../priv/ssr",
}),
react(),
],
})Modifying the CSR entrypoint
When SSR is enabled, hydrateRoot should be used.
// assets/src/js/app.jsx
import "vite/modulepreload-polyfill"
import "@fontsource-variable/instrument-sans"
import "../css/app.css"
import axios from "axios";
import { createInertiaApp } from "@inertiajs/react";
- import { createRoot } from "react-dom/client";
+ import { createRoot, hydrateRoot } from "react-dom/client";
axios.defaults.xsrfCookieName = "CSRF-TOKEN"
axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN"
+ function ssr_mode() {
+ return document.documentElement.hasAttribute("data-ssr");
+ }
createInertiaApp({
resolve: (name) => {
const page = `./pages/${name}.jsx`
const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
return pages[page]
},
setup({ el, App, props }) {
- createRoot(el).render(<App {...props} />)
+ if (ssr_mode()) {
+ hydrateRoot(el, <App {...props} />);
+ } else {
+ createRoot(el).render(<App {...props} />)
+ }
},
})Updating npm script
Update the build script in package.json to build the new ssr.js file.
"scripts": {
"dev": "vite",
- "build": "vite build",
+ "build": "vite build && vite build --ssr",
// ...
},Now you can build both your client-side and server-side bundles.
$ npm run build
Updating .gitignore
Since priv/ssr/ is for generated file, add it to your .gitignore file.
# .gitignore
+ # Ignore files that are produced for SSR by assets build tools.
+ /priv/ssr/Server-side setup
Setting up Combo.Inertia.SSR
First, add the Combo.Inertia.SSR module to the of supervision tree:
# lib/my_app/web/supervisor.ex
defmodule MyApp.Web.Supervisor do
use Supervisor
@spec start_link(term()) :: Supervisor.on_start()
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl Supervisor
def init(_arg) do
children =
Enum.concat(
inertia_children(),
[
MyApp.Web.Endpoint
]
)
Supervisor.init(children, strategy: :one_for_one)
end
defp inertia_children do
config = Application.get_env(:my_app, MyApp.Web.Endpoint)
ssr? = get_in(config, [:inertia, :ssr])
if ssr? do
path = Path.join([Application.app_dir(:my_app), "priv/ssr"])
[{Combo.Inertia.SSR, endpoint: MyApp.Web.Endpoint, path: path}]
else
[]
end
end
endThen, update your config to enable SSR for production environment:
# config/prod.exs
config :my_app, MyApp.Web.Endpoint,
inertia: [
ssr: true
]Setting up Inertia helper (optional)
Create a little helper can be used in the resolve function.
First, assets/src/js/inertia-helper.js:
// An Inertia helper for resolving page component.
//
// # Usage
//
// Use it in the `resolve` function.
//
// ## Resolve page component
//
// createInertiaApp({
// // ...
// resolve: (name) =>
// resolvePageComponent(
// `./pages/${name}.jsx`,
// import.meta.glob("./pages/**/*.jsx", { eager: true }),
// ),
// })
//
// ## Resolve page component with a fallback name
//
// createInertiaApp({
// // ...
// resolve: (name) =>
// resolvePageComponent(
// `./pages/${name}.jsx`,
// import.meta.glob("./pages/**/*.jsx", { eager: true }),
// { fallbackName: "./pages/404.jsx" },
// ),
// })
//
export async function resolvePageComponent(name, pages, options) {
if (typeof pages[name] === "undefined" && options?.fallbackName) {
name = options.fallbackName
}
const page = pages[name]
if (typeof page !== "undefined") {
// When code spiltting is enabled, page is a function.
// Or, page is an object.
return typeof page === "function" ? page() : page
} else {
throw new Error(`Page not found: ${name}`)
}
}Then, use it in your assets/src/js/app.jsx and assets/src/js/ssr.jsx.
Rendering responses
Rendering an Inertia response looks like this:
defmodule MyApp.Web.PageController do
use MyApp.Web, :controller
def index(conn, _params) do
conn
|> inertia_put_prop(:msg, "Hello world")
|> inertia_render("Home")
end
endShared data
To share data on every request, you can use the inertia_put_prop/3 function inside of a plug in your response pipeline. For example, suppose you have a UserAuth plug responsible for fetching the current user and you want to be sure all your Inertia components receive that user data. Your plug can be something like this:
defmodule MyApp.Web.UserAuth do
import Plug.Conn
import Combo.Conn
import Combo.Inertia.Conn
def authenticate_user(conn, _opts) do
user = get_user_from_session(conn)
conn
|> assign(:user, user)
# put a serialized represention of the user to Inertia props.
|> inertia_put_prop(:user, serialize_user(user))
end
# ...
endAnywhere this plug is used, the serialized user prop will be passed to the Inertia components.
Lazy data evaluation
Deferred props
Merging props
Once props
Scroll props
History encryption
Global encryption
To enable history encryption globally, use:
config :my_app, MyApp.Web.Endpoint,
inertia: [
encrypt_history: true
]Per-request encryption
To encrypt the history of an individual request, use:
Clearing history
To clear the history state, use:
Distinctive features
Camelizing props
Combo.Inertia allows to automatically convert your prop keys from snake case (conventional in Elixir) to camel case (conventional in JavaScript), like first_name to firstName.
To configure it globally:
import Config
config :my_app, MyApp.Web.Endpoint
inertia: [
camelize_props: true
]To configure it on a per-request basis.
defmodule MyApp.Web.PageController do
use MyApp.Web, :controller
def index(conn, _params) do
conn
|> inertia_put_prop(:first_name, "Bob")
|> inertia_camelize_props()
|> inertia_render("Welcome")
end
endFlash messages
Combo.Inertia automatically includes Combo flash data in Inertia props, under the flash key.
For example, given the following controller action:
def update(conn, params) do
case MyApp.Settings.update(params) do
{:ok, _settings} ->
conn
|> put_flash(:info, "Settings updated")
|> redirect(to: "/settings")
{:error, changeset} ->
conn
|> inertia_put_errors(changeset)
|> redirect(to: "/settings")
end
endWhen redirecting to the /settings page, the Inertia component will receive the flash prop:
{
"component": "...",
"props": {
"flash": {
"info": "Settings updated"
},
// ...
}
}Validations
Validation errors follow some specific conventions to make wiring up with Inertia's form helpers seamless. The errors prop is managed by Combo.Inertia and is always included in the props object for Inertia components. (When there are no errors, the errors prop will be an empty object).
The inertia_put_errors function is how you tell Inertia what errors should be represented on the front-end. By default, you can either pass an Ecto.Changeset struct or a bare map to it. For other error data types, you may implement the Combo.Inertia.Errors protocol:
def update(conn, params) do
case MyApp.Settings.update(params) do
{:ok, _settings} ->
conn
|> put_flash(:info, "Settings updated")
|> redirect(to: "/settings")
{:error, changeset} ->
conn
|> inertia_put_errors(changeset)
|> redirect(to: "/settings")
end
endThe inertia_put_errors function will convert the changeset errors into a shape compatible with the client-side adapter. Since Inertia expects a flat map of key-value pairs, the error serializer will flatten nested errors down to compound keys:
{
"name" => "can't be blank",
// Nested errors keys are flattened with a dot separator (`.`)
"team.name" => "must be at least 3 characters long",
// Nested arrays are zero-based and indexed using bracket notation (`[0]`)
"items[1].price" => "must be greater than 0"
}Errors are automatically preserved across redirects, so you can safely respond with a redirect back to page where the form lives to display form errors.
If you need to construct your own map of errors (rather than pass in a changeset), be sure it's a flat mapping of atom (or string) keys to string values like this:
conn
|> inertia_put_errors(%{
name: "Name can't be blank",
password: "Password must be at least 5 characters"
})Testing
We recommend importing Combo.Inertia.Testing in your ConnCase helper:
defmodule MyApp.Web.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
import Combo.Inertia.Testing
# ...
end
end
endDeployment
There's only one thing to note - make Node.js running in production mode, which is configured by setting following environment variable:
NODE_ENV="production"Why? In short:
- To get best SSR performance. Node.js running in production mode will cache the SSR module in memory.
- To avoid memory leaks.
Performance comparison for rendering a simple page when testing on an M1 MacBook Pro:
- Node.js running in production mode -
4ms- Node.js running in non-production mode -
15ms
More
Visit https://inertiajs.com/.