Server-side Rendering
By default, Yew components render on the client side. When a viewer visits a website, the server sends a skeleton HTML file without any actual content and a WebAssembly bundle to the browser. Everything is rendered on the client side by the WebAssembly bundle. This is known as client-side rendering.
This approach works fine for most websites, with some caveats:
- Users will not be able to see anything until the entire WebAssembly bundle is downloaded and the initial render has been completed. This can result in a poor experience for users on a slow network.
- Some search engines do not support dynamically rendered web content and those who do usually rank dynamic websites lower in the search results.
To solve these problems, we can render our website on the server side.
How it Works
Yew provides a ServerRenderer to render pages on the
server side.
To render Yew components on the server side, you can create a renderer
with ServerRenderer::<App>::new() and call renderer.render().await
to render <App /> into a String.
use yew::prelude::*;
use yew::ServerRenderer;
#[component]
fn App() -> Html {
html! {<div>{"Hello, World!"}</div>}
}
// we use `flavor = "current_thread"` so this snippet can be tested in CI,
// where tests are run in a WASM environment. You likely want to use
// the (default) `multi_thread` favor as:
// #[tokio::main]
#[tokio::main(flavor = "current_thread")]
async fn no_main() {
let renderer = ServerRenderer::<App>::new();
let rendered = renderer.render().await;
// Prints: <div>Hello, World!</div>
println!("{}", rendered);
}
Component Lifecycle
The recommended way of working with server-side rendering is function components.
All hooks other than use_effect (and use_effect_with)
will function normally until a component successfully renders into Html
for the first time.
Web APIs such as web_sys are not available when your component is
rendering on the server side.
Your application will panic if you try to use them.
You should isolate logics that need Web APIs in use_effect or
use_effect_with as effects are not executed during server-side rendering.
While it is possible to use Struct Components with server-side rendering,
there are no clear boundaries between client-side safe logic like the
use_effect hook for function components and lifecycle events are invoked
in a different order than the client side.
In addition, Struct Components will continue to accept messages until all of its
children are rendered and destroy method is called. Developers need to
make sure no messages possibly passed to components would link to logic
that makes use of Web APIs.
When designing an application with server-side rendering support, prefer function components unless you have a good reason not to.
Data Fetching during Server-side Rendering
Data fetching is one of the difficult points with server-side rendering and hydration.
Traditionally, when a component renders, it is instantly available (outputs a virtual DOM to be rendered). This works fine when the component does not want to fetch any data. But what happens if the component wants to fetch some data during rendering?
In the past, there was no mechanism for Yew to detect whether a component is still fetching data. The data-fetching client is responsible to implement a solution to detect what is being requested during the initial render and triggers a second render after requests are fulfilled. The server repeats this process until no more pending requests are added during a render before returning a response.
This not only wastes CPU resources by repeatedly rendering components, but the data client also needs to provide a way to make the data fetched on the server side available during the hydration process to make sure that the virtual DOM returned by the initial render is consistent with the server-side rendered DOM tree which can be hard to implement.
Yew takes a different approach by trying to solve this issue with <Suspense />.
Suspense is a special component that when used on the client side, provides a way to show a fallback UI while the component is fetching data (suspended) and resumes to normal UI when the data fetching completes.
When the application is rendered on the server side, Yew waits until a component is no longer suspended before serializing it into the string buffer.
During the hydration process, elements within a <Suspense /> component
remains dehydrated until all of its child components are no longer
suspended.
With this approach, developers can build a client-agnostic, SSR-ready application with data fetching with very little effort.
Low-level hooks
Yew ships two low-level hooks for carrying server-computed state to the client:
use_prepared_state!runs an (optionally async) closure during SSR, serializes the result, and delivers it to the client during hydration. Ideal for fetching data that the component needs on first render.use_transitive_state!is similar, but the closure runs after the component's SSR output is produced. Useful for collecting caches or aggregated state.
Both use bincode + base64 under the hood and are embedded in the HTML as <script> tags.
yew-link: unified data fetching
The low-level hooks handle the initial page load (SSR → hydration), but after hydration, client-side navigation requires a separate data-fetching path (e.g. use_future_with + an HTTP client). This means writing the same data logic twice.
The yew-link crate provides a higher-level abstraction that unifies all three paths (SSR, hydration, and client-side navigation) behind a single hook:
- Define your data type with the
#[linked_state]attribute macro. - Register a resolver on the server.
- Wrap your app in
<LinkProvider>. - Call
use_linked_state::<MyData>(input)in any component.
use yew_link::{linked_state, LinkedState};
#[derive(Clone, Serialize, Deserialize)]
pub struct Post { pub title: String, pub body: String }
#[linked_state]
impl LinkedState for Post {
type Context = DbPool;
type Input = u32;
async fn resolve(ctx: &DbPool, id: &u32) -> Self {
ctx.get_post(*id).await
}
}
The macro generates the LinkedState and (server-only) LinkedStateResolve trait impls. The resolve body is stripped from WASM bundles automatically.
Typed errors
If resolve can fail, declare type Error:
#[linked_state]
impl LinkedState for Post {
type Context = DbPool;
type Input = u32;
type Error = ApiError;
async fn resolve(ctx: &DbPool, id: &u32) -> Result<Self, ApiError> {
ctx.get_post(*id).await.map_err(ApiError::from)
}
}
When type Error is omitted, the error type defaults to an uninhabited Never type and the resolve body is wrapped in Ok(…).
use_linked_state returns SuspensionResult<Result<Rc<T>, LinkError<T::Error>>>. The outer layer is for suspense (loading), the inner Result carries the resolved value or a LinkError that distinguishes application errors (LinkError::Resolve) from infrastructure failures (LinkError::Internal).
Server setup
use yew_link::{Resolver, linked_state_handler};
let resolver = Arc::new(
Resolver::new()
.register_linked::<Post>(db_pool.clone())
);
let app = axum::Router::new()
.route("/api/link", axum::routing::post(linked_state_handler))
.with_state(resolver);
Component usage
use yew_link::{use_linked_state, LinkProvider};
#[component]
fn PostPage(props: &PostPageProps) -> HtmlResult {
let post = use_linked_state::<Post>(props.id)?.unwrap();
Ok(html! { <h1>{ &post.title }</h1> })
}
During SSR the state is resolved locally via the Resolver and embedded in the HTML through use_prepared_state. On hydration the client reads the embedded state with zero network requests. On subsequent client-side navigations the hook fetches from the LinkProvider's endpoint URL automatically.
See the ssr_router example for a full working demo.
Rendering <head> Tags
A common need with SSR is rendering dynamic <head> content (e.g. <title>,
<meta>) so that crawlers and social previews see the right metadata on first
load.
ServerRenderer only renders your component tree (typically at the body of the document),
it has no access to <head>. Head tags must therefore be generated on the server, outside of
Yew, and spliced into the HTML template before it is sent to the client.
The ssr_router example demonstrates this pattern: the server recognizes the
route from the request URL, generates the appropriate <title> and <meta>
tags, and injects them into the Trunk-generated index.html before
</head>.
For a fully SSR-compatible third-party solution, use the <Helmet/> component from Bounce.
SSR Hydration
Hydration is the process that connects a Yew application to the
server-side generated HTML file. By default, ServerRender prints
hydratable HTML string which includes additional information to facilitate hydration.
When the Renderer::hydrate method is called, instead of starting rendering from
scratch, Yew will reconcile the Virtual DOM generated by the application
with the HTML string generated by the server renderer.
To successfully hydrate an HTML representation created by the
ServerRenderer, the client must produce a Virtual DOM layout that
exactly matches the one used for SSR including components that do not
contain any elements. If you have any component that is only useful in
one implementation, you may want to use a PhantomComponent to fill the
position of the extra component.
The hydration can only succeed if the real DOM matches the expected DOM
after initial render of the SSR output (static HTML) by browser. If your HTML is
not spec-compliant, the hydration may fail. Browsers may change the DOM structure
of the incorrect HTML, causing the actual DOM to be different from the expected DOM.
For example, if you have a <table> without a <tbody>, the browser may add a <tbody> to the DOM
Component Lifecycle during hydration
During Hydration, components schedule 2 consecutive renders after it is
created. Any effects are called after the second render completes.
It is important to make sure that the render function of your
component is free of side effects. It should not mutate any states or trigger
additional renders. If your component currently mutates states or triggers
additional renders, move them into a use_effect hook.
It is possible to use Struct Components with server-side rendering in
hydration, the view function will be called
multiple times before the rendered function will be called.
The DOM is considered as not connected until the rendered function is called,
you should prevent any access to rendered nodes
until rendered() method is called.
Example
use yew::prelude::*;
use yew::Renderer;
#[component]
fn App() -> Html {
html! {<div>{"Hello, World!"}</div>}
}
fn main() {
let renderer = Renderer::<App>::new();
// 直接在 body 元素下注水,并移除可能有的任何尾随元素。
renderer.hydrate();
}
Example: simple_ssr Example: ssr_router
Single thread mode
Yew supports single thread mode for server-side rendering by yew::LocalServerRenderer. This mode would work in a single thread environment like WASI.
// Build using `wasm32-wasip1` target or `wasm32-wasip2` target.
use yew::prelude::*;
use yew::LocalServerRenderer;
#[component]
fn App() -> Html {
use yew_router::prelude::*;
html! {
<>
<h1>{"Yew WASI SSR demo"}</h1>
</>
}
}
pub async fn render() -> String {
let renderer = LocalServerRenderer::<App>::new();
let html_raw = renderer.render().await;
let mut body = String::new();
body.push_str("<body>");
body.push_str("<div id='app'>");
body.push_str(&html_raw);
body.push_str("</div>");
body.push_str("</body>");
body
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("{}", render().await);
}
Example: wasi_ssr_module
If you are using the wasm32-unknown-unknown target to build a SSR application, you can use the not_browser_env feature flag to disable access of browser-specific APIs inside of Yew. This would be useful on serverless platforms like Cloudflare Worker.
Server-side rendering is currently experimental. If you find a bug, please file an issue on GitHub.