Introduction
Inertia Rust is a Rust adapter for Inertia.js.
It’s not its aim to work around a single specific framework. Instead, it’s built out of providers, which are responsible for implementing the crate’s traits for a given framework.
Currently, only actix-web has a provider. As it’s the first — and the only — existing provider, every code block will be considering it as the underlying framework. Note, however, that the code shouldn’t be very different depending on the opted provider.
We will cover only how to make things that are specific to Inertia Rust. For the front-end, refer to Inertia’s oficial documentation.
Note: This documentation describes how to use
inertia-rust v2.x
, which is compatible with Inertia.js v2.0.0. Also, it has notable differences comparing to the former v0.1.0.Indeed, we went from v0.1.0 straight to v2.0.0. The reason is we wanted to let it as clear as possible that this is the correct version of the crate to work with Inertia.js v2.
Inertia Rust v0.1.0
If you want to use Inertia.js 1, you must refer to a previous version of Inertia Rust. Unfortunately, there is no such a documentation about it. We hope you can make it using the following files:
File | Content |
---|---|
|
|
Not actually a requirements list for you to use Inertia, but of an lib to be a useful Inertia Adapter:
|
|
A changelog of every change that has occurred between v0.1.0 and v2.0.0, including the good-to-know breaking changes. Note that developer experience — DX — has been drastically enhanced from the former to the latter version. |
Installation
To get Inertia working, you’ll need to ensure some peer dependencies are installed. Once you opt-in a specific provider, assure the needed peer crate is also available.
# Cargo.toml
[dependencies]
inertia-rust = { version = "2", features = ["actix", "vite-template-resolver"] }
actix-web = "4"
vite-rust = { version = "0.2" }
The vite-template-resolver feature enables the ViteTemplateResolver
. We’ll discuss it furthermore, in the
Template Resolvers, along with how to set up your own template resolver. On this documentation, we’ll use
vite-rust and Actix Web, so you must have them installed.
Available Crate Features
actix
: enable Actix Web provider;vite-template-resolver
: enableViteTemplateResolver
.
Vite Setup
ViteTemplateResolver
requires a Vite
instance, so, we need to both set up Vite and Inertia. Read vite-rust
docs for more details about setting it up. For this example, consider the following configuration:
// src/config/vite.rs
use vite_rust::{Vite, ViteConfig};
pub async fn initialize_vite() -> Vite {
let vite_config = ViteConfig::default()
.set_manifest_path("path/to/manifest.json")
// you can add the same client-side entrypoints to vite-rust,
// so that it won't panic if the manifest file doesn't exist but the
// development server is running
.set_entrypoints(vec!["www/app.ts"]);
match Vite::new(vite_config).await {
Err(err) => panic!("{}", err),
Ok(vite) => vite
}
}
Inertia Setup
// src/config/inertia.rs
use super::vite::initialize_vite;
use inertia_rust::{
template_resolvers::ViteTemplateResolver, Inertia, InertiaConfig, InertiaVersion,
};
use std::io;
pub async fn initialize_inertia() -> Result<Inertia, io::Error> {
let vite = initialize_vite().await;
let version = vite.get_hash().unwrap_or("development").to_string();
let resolver = ViteTemplateResolver::new(vite);
Inertia::new(
InertiaConfig::builder()
.set_url("http://localhost:3000")
.set_version(InertiaVersion::Literal(version))
.set_template_path("www/root.html")
.set_template_resolver(Box::new(resolver))
.build(),
)
}
Inertia Configuration
Option | Type (default) | Description |
---|---|---|
url | &str | A valid href of the current application |
version | InertiaVersion | The current asset version of the application. See Asset versioning for more details. |
template_path | &str | The path to the root html template. |
template_resolver | Box<dyn TemplateResolver + Send + Sync> | A valid Template Resolver. |
with_ssr | bool (false ) | Whether Server-side Rendering should be enabled or not. |
custom_ssr_client | Option<SsrClient> (SsrClient::default ) | The Inertia Server address. |
view_data | Option<ViewData> (None ) | Optional view data to be passed to the root template. (needs to be handled by the Template Resolver) |
encrypt_history | bool (false ) | Whether to encrypt the session or not. Refer to History encryption for more details. |
For even more details, read the InertiaConfig
and InertiaConfigBuilder
documentations.
Actix Web Server
// src/main.rs
use std::sync::{Arc, OnceLock};
use actix_web::{dev::Path, web::Data, App, HttpServer};
use config::inertia::initialize_inertia;
mod config;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok();
env_logger::init();
// starts a Inertia manager instance.
let inertia = initialize_inertia().await?;
let inertia = Data::new(inertia);
HttpServer::new(move || App::new().app_data(inertia.clone()))
.bind(("127.0.0.1", 3000))?
.run()
.await
}
Root Template
ViteTemplateResolver
receives a path to an HTML template file. Inertia Rust will pass the given template_path
to the resolver. In this case, it’s www/root.html
file.
<!-- www/root.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- include this if you're using ReactJs -->
<!-- @vite::react -->
@vite
@inertia::head
</head>
<body>
@inertia::body
</body>
</html>
Inertia Middleware
The Inertia Middleware comes from your opted provider. It has few responsibilities:
- allow you to share props, via
with_shared_props
method (discussed in Shared Props); - ensure that redirects for
PUT
,PATCH
andDELETE
requests always use a 303 status code; - merge shared props with errors flash messages (discussed in Flash Messages and Validation Errors).
use actix_web::{App, HttpServer};
use inertia_rust::actix::InertiaMiddleware;
use inertia_rust::{hashmap, InertiaProp, InertiaProps, InertiaService};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ...
HttpServer::new(move || {
App::new()
.app_data(inertia.clone())
.wrap(InertiaMiddleware::new())
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
You can check more examples in Inertia Rust examples directory. Also, do check Inertia.js client-side setup tutorial.
Server-Side Rendering
Inertia Rust supports server-side rendering. Enabling SSR gives your website better SEO, since the very first page will be server-side rendered. Thereafter, they’ll be ordinary SPA pages.
Note: Node.js must be available in order to run the Inertia SSR server.
Enabling SSR is a very simple task. However, you must make few changes in your code so that the Node.js process is correctly started and killed — otherwise, the Node.js process would be left running and not letting one start the new server on the given port.
First of all, enable SSR in your Inertia initialization function:
// src/config/inertia.rs
use super::vite::initialize_vite;
use inertia_rust::{
template_resolvers::ViteTemplateResolver, Inertia, InertiaConfig, InertiaVersion, SsrClient,
};
use std::io;
pub async fn initialize_inertia() -> Result<Inertia, io::Error> {
let vite = Arc::new(initialize_vite().await);
let version = vite.get_hash().unwrap_or("development").to_string();
let resolver = ViteTemplateResolver::new(vite.clone());
Inertia::new(
InertiaConfig::builder()
.set_url("http://localhost:3000")
.set_version(InertiaVersion::Literal(version))
.set_template_path("www/root.html")
.set_template_resolver(Box::new(resolver))
// note these two lines
.enable_ssr()
// `set_ssr_client` is optional. If not set, `SsrClient::default()` will be used,
// which is is "127.0.0.1:13714"
.set_ssr_client(SsrClient::new("127.0.0.1", 1000))
.build())
}
// src/main.rs
use std::sync::{Arc, OnceLock};
use actix_web::{dev::Path, web::Data, App, HttpServer};
use config::inertia::initialize_inertia;
mod config;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok();
env_logger::init();
// starts a Inertia manager instance.
let inertia = initialize_inertia().await?;
let inertia = Data::new(inertia);
let inertia_clone = inertia.clone();
let server = HttpServer::new(move || App::new().app_data(inertia_clone.clone()))
.bind(("127.0.0.1", 3000))?;
// Starts a Node.js child process that runs the Inertia's server-side rendering server.
// It must be started after the server initialization to ensure that the http server won't
// panic and shutdown without killing the Node.Js process.
let node = inertia.start_node_server("path/to/your/ssr.js".into())?;
let server = server.run().await;
let _ = node.kill().await;
return server;
Indeed, you can replace let _ = node.kill().await;
with std::mem::drop(node.kill())
, but .await
ing it
guarantees the process is killed.
The Facades
Inertia Rust has several traits that describe its behavior. The provider’s job is to implement them to the framework structs, connecting Inertia Rust to it.
The main trait describes the operations inner_render
, inner_render_with_props
and inner_location
,
which must be implemented and contains the actual methods (the implementations of the operations).
For using these functions, you’d have to extract the Inertia
instance from the request context and,
again, pass a reference to the whole http request to it.
It’s definetely far from being fancy. That’s why we also provide — and require provides to implement
— the InertiaFacade
trait. It contains the render
, render_with_props
and location
operations.
The provider uses the given request reference to extract the Inertia
instance itself and call the
respective method.
It’s way more elegant to do this:
use inertia_rust::{Inertia, InertiaFacade};
use actix_web::{get, Responder, HttpRequest};
#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
Inertia::render(&req, "Index".into()).await
}
Than this:
use inertia_rust::Inertia;
use actix_web::{get, Responder, HttpRequest, web::Data};
#[get("/")]
async fn index(req: HttpRequest, inertia: Data<Inertia>) -> impl Responder {
inertia.inner_render(&req, "https://inertiajs.com").await
}
Routes and Responses
Creating routes is totally up to the framework you’re using for building your application, of course. However, we provide some facilities. For instance, you can render a simple page both ways:
use inertia_rust::{Inertia, InertiaFacade, InertiaService};
use actix_web::{app, get, Responder, HttpRequest};
// this
App::new().inertia_route("/", "Index");
// is the same as this
#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
Inertia::render(&req, "Index".into()).await
}
App::new().service(index);
Redirects
If you want to redirect to another page, it’s enough to return your framework Redirect response. However, when
redirecting to an external site, make sure to use Inertia::location
method. It’ll defer the redirect to the
browser:
use inertia_rust::{Inertia, InertiaFacade};
use actix_web::{get, Responder, HttpRequest};
#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
Inertia::location(&req, "https://inertiajs.com")
}
To redirect to the previous page (a.k.a redirect back), use the back
facade method:
use inertia_rust::{Inertia, InertiaFacade};
use actix_web::{get, Responder, HttpRequest};
#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
Inertia::back(&req)
}
If you’ve configured your Inertia Temporary Session middleware, it will use the previous visited page with
more accuracy. Otherwise, it’ll look for a referer
header. In last case, it will redirect to /
.
Redirecting back with props is a little bit of advanced. Refer to Redirecting Back With Errors for an detailed explanation.
Responses
Calling the facade render
method render a page (be it ReactJs, Svelte or Vue.js):
use inertia_rust::{Inertia, InertiaFacade};
use actix_web::{get, Responder, HttpRequest};
#[get("/users")]
async fn index(req: HttpRequest) -> impl Responder {
Inertia::render(&req, "Users/Index".into()).await
}
"Users/Index"
is a Component
— which is just a meaningful struct containing the name of the jsx, vue or
svelte component to be rendered. Naturally, you must have configured your www/app.ts
file to find this
page component (again, remember to check Inertia.js documentation).
For example, it could be:
import { Head } from "@inertiajs/react";
type User = {
name: string;
email: string;
}
type UsersPageProps = {
users: User[];
}
export default function UsersPage({ user }: UsersPageProps) {
return (
<>
<Head title="Users" />
<div>
<h1>Users</h1>
{users.map(user => (
<div>
<span><strong>Name: </strong>{user.name}</span>
<span><strong>E-mail: </strong>{user.email}</span>
</div>
))}
</div>
</>
)
}
You’ve noticed that this page has a user
property. Actually, Inertia Rust is the one who will provide
this property! We didn’t provide any props to the page, though. We’ll fix this in the next chapter, where
props are discussed.
Props
It’s possible to pass data from the server to the front-end as simple props. To do this, simply
create a HashMap
with &str
keys and InertiaProp
values. Then, use Inertia::render_with_props
passing the props hashmap as the third parameter.
Also, for defining the props hash map, we provide a very trivial macro: hashmap!
. You can use it
like this:
use inertia_rust::{hashmap, Inertia, InertiaFacade, InertiaProp};
use actix_web::{get, web, HttpRequest, Responder};
use serde_json::{json};
use serde::Serialize;
#[derive(Serialize)]
struct User {
pub name: String,
pub email: String,
}
impl User {
// let's just pretend this is a method that fetch users from some database
pub async fn all() -> Vec<User> {
vec![
User {
name: "John Doe".into(),
email: "johndoe@gmail.com".into()
},
User {
name: "Ada Lovelace".into(),
email: "thereWereNoEmailByThatTimeActually!@gmail.com".into(),
}
]
}
}
#[get("/users")]
async fn home(req: HttpRequest) -> impl Responder {
let props = hashmap![
"users" => InertiaProp::data(User::all().await),
];
Inertia::render_with_props(&req, "Users/Index".into(), props).await
}
Indeed, all hashmap!
do is to create a std::collections::HashMap
and insert each item you’ve passed
into it.
Note that a new enum has been introduced to our code right now: InertiaProp
. It contains all the variants
of props that Inertia can handle.
Every InertiaProp
variant must contain — or resolve to — an Result<serde_json::Value, InertiaError
>
object. You can achieve this by calling value.into_inertia_value()
. value
must be serializable. Also,
you must bring the IntoInertiaPropResult
trait into the scope.
If you’re using InertiaProp
helpers, you don’t even need to worry about serializing by yourself (again,
since the object is serializable):
use inertia_rust::{InertiaProp, IntoInertiaPropResult};
let prop = InertiaProp::Data("foo".into_inertia_value());
// or
let prop = InertiaProp::data("foo"); // using `data` helper for generating a `InertiaProp::Data`
On the official Inertia.js documentation, you may find the following PHP snippet:
return Inertia::render('Users/Index', [
// ALWAYS included on standard visits
// OPTIONALLY included on partial reloads
// ALWAYS evaluated
'users' => User::all(),
// ALWAYS included on standard visits
// OPTIONALLY included on partial reloads
// ONLY evaluated when needed
'users' => fn () => User::all(),
// NEVER included on standard visits
// OPTIONALLY included on partial reloads
// ONLY evaluated when needed
'users' => Inertia::lazy(fn () => User::all()),
// ALWAYS included on standard visits
// ALWAYS included on partial reloads
// ALWAYS evaluated
'users' => Inertia::always(User::all()),
]);
You can achieve the exact same behaviors with Inertia Rust:
return Inertia::render_with_props(&req, "Users/Index", hashmap![
// ALWAYS included on standard visits
// OPTIONALLY included on partial reloads
// ALWAYS evaluated
"users" => InertiaProp::data(User::all().await),
// ALWAYS included on standard visits
// OPTIONALLY included on partial reloads
// ONLY evaluated when needed
"users" => InertiaProp::lazy(prop_resolver!({ User::all().await.into_inertia_value() })),
// NEVER included on standard visits
// OPTIONALLY included on partial reloads
// ONLY evaluated when needed
"users" => InertiaProp::demand(prop_resolver!({ User::all().await.into_inertia_value() })),
// ALWAYS included on standard visits
// ALWAYS included on partial reloads
// ALWAYS evaluated
"users" => InertiaProp::always(User::all().await),
]);
The prop_resolver!
macro
Inertia props that take callbacks — such as Lazy
, Demand
and Deferred
variants — are
asynchronous, so that you can .await
inside of them. To use it, it’d be necessary the following code:
use std::sync::Arc;
use inertia_rust::InertiaProp;
use inertia_rust::IntoInertiaPropResult;
let lazy_prop = InertiaProp::Lazy(Arc::new(move || Box::pin(async move {
User::all().await.into_inertia_value();
})));
Such a massive amount of code, we know. However, it’s necessary in order to have asynchronous callbacks support.
In order to avoid writting all this boilerplate, Inertia Rust provide another macro: prop_resolver!
,
as you’ve seen above:
use std::sync::Arc;
use inertia_rust::{prop_resolver, InertiaProp, IntoInertiaPropResult};
let lazy_prop = InertiaProp::Lazy(prop_resolver!({ User::all().await.into_inertia_value() }));
There are some cases — mainly in test environments or playgrounds — where you might want to mock some database and, therefore, need to move values to inside of the resolver closure:
use std::sync::Arc;
use inertia_rust::InertiaProp;
use inertia_rust::IntoInertiaPropResult;
let user = Arc::new(User { name: "John Doe".into(), email: "johndoe@gmail.com".into() });
let permissions = Arc::new(vec!["read", "delete", "update", "delete"]);
InertiaProp::lazy(Arc::new(move || {
let user = user.clone();
let permissions = permissions.clone();
Box::pin(async move {
user_can(user, permissions).await.into_inertia_value()
})
}));
Unfortunately, we have to move a clone of the Arc
-wrapped variables to inside of the closure,
then move again to inside of the inner async closure.
prop_resolver!
also covers this. As first parameter, you’ll pass the “cloning” statements separated
by commas. The actual async closure goes as the second parameter, then:
use std::sync::Arc;
use inertia_rust::{prop_resolver, InertiaProp, IntoInertiaPropResult};
let user = Arc::new(User { name: "John Doe".into(), email: "johndoe@gmail.com".into() });
let permissions = Arc::new(vec!["read", "delete", "update", "delete"]);
InertiaProp::lazy(prop_resolver!(
let user = user.clone(), // statements separated by comma
let permissions = permissions.clone(); // statements and block separated by semicolon
{ user_can(user, permissions).await.into_inertia_value() } // block
));
Shared Props
There are some props that are sent to the front-end on every request. These can be easily shared through
InertiaMiddleware
rather than conventional props on each request.
The middleware contains a with_shared_props
method, which requires a callback that receives a reference
to the current HTTP request. You can use it to extract any information you might want to share.
use actix_web::{App, HttpServer};
use inertia_rust::actix::InertiaMiddleware;
use inertia_rust::{hashmap, InertiaProp, InertiaProps, InertiaService};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// ...
HttpServer::new(move || {
App::new()
.app_data(inertia.clone())
.wrap(InertiaMiddleware::new().with_shared_props(Arc::new(|req| {
// get the sessions from the request
// depending on your opted framework
let session = req.get_session();
let flash = session.get::<String>("flash").unwrap();
async move {
hashmap![ "flash" => InertiaProp::data(flash) ]
}
.boxed_local()
})))
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
Deferred Props
Deferred props behave the exactly same way as InertiaProp::Demand
. However, when you use Deferred
instead of
Demand
, Inertia router will automatically make a partial request to obtain the deferred props after the page
loads.
It means deferred data will be loaded after the page has already been rendered, so if you defer heavy or time- consuming data, your page might load faster.
Deferring a Prop
use actix_web::{get, HttpRequest, Responder};
use inertia_rust::{hashmap, prop_resolver, Inertia, InertiaProp, IntoInertiaPropResult};
// let's pretend these are some ORM's models and
// assume they have an `all` method.
use domain::identity::models::Role;
use domain::identity::models::User;
use domain::identity::models::Permission;
#[get("/users")]
pub async fn users(req: HttpRequest) -> impl Responder {
Inertia::render_with_props(&req, "Users/Index", hashmap![
'users' => InertiaProp::data(User::all().await),
'roles' => InertiaProp::data(Role::all().await),
'permissions' => InertiaProp::defer(prop_resolver!({ Permission::all().await.into_inertia_value() })),
]).await
}
Grouping Requests
Deferred prop are splitten in groups. Each group is paralelly requested by the client Inertia router.
InertiaProp::defer
will put the resolver in the "default"
group of deferred props. If you want to
create a custom group, call InertiaProp::defer_with_group
passing the group name as the second parameter:
#[get("/users")]
pub async fn users(req: HttpRequest) -> impl Responder {
Inertia::render(&req, "Users/Index", hashmap![
'users' => User::all(),
'roles' => Role::all(),
'permissions' => InertiaProp::defer(prop_resolver!({ Permission::all().await.into_inertia_value() })),
'teams' => InertiaProp::defer_with_group(prop_resolver!({ Team::all().await.into_inertia_value() }), 'attributes'),
'projects' => InertiaProp::defer_with_group(prop_resolver!({ Project::all().await.into_inertia_value() }), 'attributes'),
'tasks' => InertiaProp::defer_with_group(prop_resolver!({ Task::all().await.into_inertia_value() }), 'attributes'),
]).await
}
Merging Props
Merging Props are merged with the existing props in the client-side, instead of overwriting them. For instance,
if your client already has a property "permissions"
that is a list ["read", "delete"]
, given that
"permissions"
is a mergeable prop, when a new partial request receives a list "permissions" = ["update"]
,
the new value of "permissions"
in the client will be ["read", "delete", "update]
instead of ["update"]
.
Making a Mergeable Prop
use actix_web::{web, get, HttpRequest, Responder};
use inertia_rust::{hashmap, Inertia, InertiaProp};
use serde::Deserialize;
#[derive(Deserialize)]
struct RequestQuery {
page: Option<usize>,
per_page: Option<usize>
}
#[get("/users")]
pub async fn users(req: HttpRequest, query: web::Query<RequestQuery>) -> impl Responder {
let page = query.page.unwrap_or(1);
let per_page = query.per_page.unwrap_or(10);
Inertia::render_with_props(&req, "Users/Index", hashmap![
'results' => InertiaProp::merge(User::paginate(page, per_page).await),
])
}
Again, let’s pretend User
is a model of some ORM which contains a paginate
method.
Deferred props can also be mergeable. The behavior will be a combination of them both: the deferred prop will be fetched in a separate request, and when it is received, it’ll be merged with the existing version of itself.
#[get("/users")]
pub async fn users(req: HttpRequest, query: web::Query<RequestQuery>) -> impl Responder {
let page = query.page.unwrap_or(1);
let per_page = query.per_page.unwrap_or(10);
return Inertia::render_with_props(&req, "Users/Index", hashmap![
'results' => InertiaProp::defer(prop_resolve!({ User::paginate(page, per_page).await })).into_mergeable(),
])
}
Resetting Props
When a mergeable property is asked to be reset, it will behave like an ordinary InertiaProp::Data
variant
— or a InertiaProp::Deferred
, if it’s a deferred and mergeable prop.
router.reload({
reset: ['results'],
//...
})
History Encryption
History encryption is a way of protecting the page data stored in the browser history. For more details about it, refer to the official Inertia.js History encryption documentation. In this page, you can see how to enable it in Inertia Rust.
Global Encryption
On your Inertia struct config, you can encrypt_history
to globally enable history encryption:
use inertia_rust::{Inertia, InertiaConfig};
let inertia = Inertia::new(
InertiaConfig::builder()
.encrypt_history()
.build());
It’ll set Inertia::encrypt_history
to true
. By default, it’s disabled.
Per-request Encryption
Call encrypt_history(bool)
to enable or disable encryption of a request before you render the page:
Inertia::encrypt_history(&req, true);
It overwrites the global configuration and even the middleware (we’ll cover it in the next topic). It
means you can use it to disable encryption for an specific route by passing false
as parameter.
Encrypt Middleware
You can import the EncryptHistoryMiddleware
from your provider and wrap some routes with it to enable
encryption for the routes within the wrapped group.
use actix_web::App;
use inertia_rust::actix::EncryptHistoryMiddleware;
App::new().wrap(EncryptHistoryMiddleware::new());
Clearing History
To clear the history state from the server-side, call clear_history
before rendering the page:
Inertia::clear_history(&req):
Again, for more information about clearing history, refere to Inertia’s Clearing history section from History encryption documentation.
Template Resolvers
Template resolvers are structs that implements Inertia Rust’s TemplateResolver
trait. They’re responsible
for rendering the root template from your application. Specially, for injecting Inertia.js head and body
into the HTML.
For instance, the default ViteTemplateResolver
(available when vite-template-resolver
feature is enabled)
will use Vite Rust crate to inject the correct HTML tags for the application assets in the HTML (e.g.: React.js
scripts, the application entrypoint script, stylesheets links, preload tags, etc). Also, it’ll handle server-
side rendering and correctly insert Inertia’s body and head in the template (if it’s SSRendered) or the
InertiaPage
in the container element (if CSRendered).
Creating a Template Resolver
You can create your own template resolver pretty easily. All you need to do is create a struct with whatever
data your template resolver needs to render a page. Then, implement TemplateResolver
trait for it.
For instance, this is a short of ViteTemplateResolver
:
use crate::{template_resolvers::TemplateResolver, InertiaError, ViewData};
use async_trait::async_trait;
use std::path::Path;
use vite_rust::{features::html_directives::ViteDefaultDirectives, Vite};
pub struct ViteTemplateResolver {
pub vite: Vite,
}
impl ViteTemplateResolver {
pub fn new(vite: Vite) -> Self {
Self { vite }
}
}
#[async_trait(?Send)]
impl TemplateResolver for ViteTemplateResolver {
async fn resolve_template(
&self,
template_path: &str,
view_data: ViewData<'_>,
) -> Result<String, InertiaError> {
let path = Path::new(template_path);
let file = match tokio::fs::read(&path).await.unwrap();
let mut html = String::from_utf8(file).unwrap()
let _ self.vite.vite_directive(&mut html);
self.vite.assets_url_directive(&mut html);
self.vite.hmr_directive(&mut html);
self.vite.react_directive(&mut html);
if let Some(ssr) = &view_data.ssr_page {
html = html.replace("@inertia::body", ssr.get_body());
html = html.replace("@inertia::head", &ssr.get_head());
} else {
let stringified_page = serde_json::to_string(&view_data.page).unwrap();
let container = format!("<div id='app' data-page='{}'></div>\n", stringified_page);
html = html.replace("@inertia::body", &container);
html = html.replace("@inertia::head", "");
}
Ok(html)
}
}
Naturally, there is no untreated .unwrap()
in the official ViteTemplateResolver
, and every error is
properly handled.
Flash Messages and Validation Errors
Inertia Middleware will also merge flash errors with the shared props. The resulting props will be injected back into the request and will be further merged again with the page props during rendering, thus, making all of them available to your client-side page component.
As said earlier, Inertia Rust is not made for one single framework and all of them actually might have
built-in sessions management. Hence, you need to built by yourself a second middleware that injects an InertiaTemporarySession
object in the request context/extensions:
// InertiaTemporarySession struct from Inertia Rust
#[derive(Clone, Serialize)]
pub struct InertiaTemporarySession {
// Optional errors hashmap
pub errors: Option<Map<String, Value>>,
// The previous request URL
// useful for redirecting back with errors
pub prev_req_url: String,
}
The middleware tries to extract this from the request context and merge it with the shared props. This
is how validation errors might get available to your page components without explicitly sending them with Inertia::render_with_props
.
It’s up to you to set up a middleware that extracts errors from the session and add them wrapped in
InertiaTemporarySession
to the request’s extensions, however, there is a useful snippet containing a
sample of this middleware in Actix Web Implementations chapter.
Reflash Session Middleware
Sometimes, Inertia Rust will trigger a forced-refresh (see Assets Management topic from Inertia.js
official documentation for more details). If there was a InertiaTemporarySession
to be treated by
that request, it’ll be lost by the nature of temporary sessions.
To avoid this, Inertia Rust wraps the current temporary session inside another struct —
InertiaSessionToReflash
. This allows you to make a middleware responsible for using it to reflash the
session. This way, the temporary session will be available for the subsequent request from the current
client.
Check a actix web implementation of this middleware at Actix Web Implementations chapter.
Redirecting Back With Errors
To redirect back with errors, ensure the Inertia Temporary Session middleware is correctly set up. It
must look for a SessionErrors
instance in the request extensions and persist it in the user’s session
inside the "_errors"
key – or whatever key choosen to represent the errors in the sessions.
Note:
"_errors"
key is expected to represent a stringified JSON object.
For instance, it would be something like the following pseudo Rust code:
use actix_web::{get, HttpRequest, Responder};
use inertia_rust::{hashmap, Inertia, InertiaFacade};
#[get("/foo")]
async fn foo(req: HttpRequest) -> impl Responder {
Inertia::back_with_errors(&req, hashmap![
"age" => to_value("You must be over 13 y.o. to access this website").unwrap()
])
}
For making it a little bit better, you might provide your own facade for redirecting back with errors. Again, it’s not possible for us to make this little helper, since it’d be necessary to introduce session management crates as dependency. Nor can we provide the utility trait so that you implement it yourself, since you can only implement your own traits for foreign struct.
However, you can refer to Actix Web Implementations
and grab a snippet containing an sample implementation of a trait that enhances Inertia
struct, providing
a back_with_errors
method.
Actix Web Implementations
Check some useful snippets of middlewares mentioned by the previous chapter.
Temporary Session Middleware (with Reflash)
This middleware uses actix sessions to manage the Inertia Rust temporary sessions. It’s responsible for:
- Before the route execution:
- Removing flash data from user’s session (errors, previous and current URLs);
- Instantiating a
InertiaTemporarySession
and injecting it to the current request;
- After the route responds:
- Check if there is a request for reflashing the current session (and reflashes it, if so);
- Otherwise, adds the new “previous” and “current” requests URLs and the
SessionErrors
to the actual user’s session.
# ./Cargo.toml
[dependencies]
serde = { version = "1.0.217", features = ["derive"]}
actix-web = "4.9.0"
actix-session = "0.10.1"
inertia-rust = { version = "2.0.0", features = ["actix"] }
log = "0.4.22"
serde_json = "1.0"
use actix_session::SessionExt;
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::Error;
use actix_web::HttpMessage;
use futures_util::future::LocalBoxFuture;
use inertia_rust::{InertiaSessionToReflash, InertiaTemporarySession, actix::SessionErrors};
use log::error;
use serde_json::Map;
use std::future::{ready, Ready};
pub struct ReflashTemporarySession;
impl<S, B> Transform<S, ServiceRequest> for ReflashTemporarySession
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = ReflashTemporarySessionMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(ReflashTemporarySessionMiddleware { service }))
}
}
pub struct ReflashTemporarySessionMiddleware<S> {
service: S,
}
const ERRORS_KEY: &str = "_errors";
const PREV_REQ_KEY: &str = "_prev_req_url";
const CURR_REQ_KEY: &str = "_curr_req_url";
impl<S, B> Service<ServiceRequest> for ReflashTemporarySessionMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let session = req.get_session();
let errors = session.remove(ERRORS_KEY).map(|errors| {
serde_json::from_str(&errors).unwrap_or_else(|err| {
error!("Failed to serialize session errors: {}", err);
Map::new()
})
});
let before_prev_url = session
.get::<String>(PREV_REQ_KEY)
.unwrap_or(None)
.unwrap_or("/".into());
let prev_url = session
.get::<String>(CURR_REQ_KEY)
.unwrap_or(None)
.unwrap_or("/".into());
// ---
let temporary_session = InertiaTemporarySession {
errors,
prev_req_url: prev_url.clone(),
};
req.extensions_mut().insert(temporary_session);
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let req = res.request();
let session = req.get_session();
let inertia_session = req.extensions_mut().remove::<InertiaSessionToReflash>();
// if it needs to reflash a temporary flash session, then
// replace data from inertia session middleware with the same as before,
// so that the further request generates the same InertiaTemporarySession,
// containing the exactly same errors, previous url, and current url.
//
// otherwise, gets the previous request's URI and stores the current one's as the next
// request "previous", moving the navigation history
let (prev_url, curr_url, optional_errors) =
if let Some(InertiaSessionToReflash(inertia_session)) = inertia_session {
(before_prev_url, inertia_session.prev_req_url, inertia_session.errors)
} else {
let errors = req
.extensions_mut()
.remove::<SessionErrors>()
.map(|SessionErrors(errors)| errors);
(prev_url, req.uri().to_string(), errors)
};
if let Some(errors) = optional_errors {
if let Err(err) = session.insert(ERRORS_KEY, inertia_session.errors) {
error!("Failed to add errors to session: {}", err);
}
}
if let Err(err) = session.insert(PREV_REQ_KEY, prev_url) {
error!("Failed to update session previous request URL: {}", err);
};
if let Err(err) = session.insert(CURR_REQ_KEY, curr_url) {
error!("Failed to update session current request URL: {}", err);
};
Ok(res)
})
}
}
Yet you need to enable your framework session middleware and manager (or your own). As errors are
retrieved by remove
method, they are only available for one request lifetime. Indeed, errors
and flash messages shouldn’t persist across multiple requests.
Note: Be sure to register this middleware always after
InertiaMiddleware
. Since actix web calls the middlewares in the opposite order they’ve been registered, doing this will ensure thatInertiaMiddleware
has the correctInertiaTemporarySession
when it’s finally executed.
For more details on how to configure actix session, refer to their own documentation.