Hydration
Hydration is the process of making the initial pre-rendered html of a website interactive.
This page is only relevant when using static or server mode.
While pre-rendering your components on the server (or at built time with 'static' mode) allows for a fast "first contentful paint" (when useful content is first displayed to the user), the site is not interactive (e.g. responding to button clicks) until the client-side rendering is started and event handlers have been attached.
In static and server mode your apps lifecycle always starts on the server, which builds your components once and render
them to html. Then when the browser has loaded your site along with additional files like .js
or images, your app is executed
again on the client to continue rendering on the client. This "picking up rendering on the client" is called Hydration.
The terms "Pre-Rendering" and "Server-Side-Rendering" can be use pretty much interchangeably. "Server" in this context just refers to whatever computer performs the initial rendering away from the user's browser. This may be an actual webserver running in some datacenter, but when using "static" mode this can also be just your computer or e.g. the machine running your ci pipeline.
Separation of Environments
One main aspect of developing a site in static
or server
mode that is important to think about and keep track of is which
component is rendered in which environment. Starting at the root of your tree, components are by default only rendered on the server.
When adding hydration to the mix, now certain components are both rendered on the server and client.
This has the following implications:
- To keep the complexity of dealing with two separate rendering environments as low as possible, it is recommended to define (conceptual or structural) boundaries of where components start to also being rendered on the client. With that anything above such a boundary in the tree can be assumed to only be rendered on the server, and anything below on both server and client.
Going forward we will call "server components" those that are only rendered on the server, and "client components" those that are also rendered on the client (note the 'also' - there are no 'only rendered on the client' components in this model).
Notice how any child of a client component is automatically also a client component.
- Once defined, components can make use of environment-specific concepts.
-
Server components can safely import and use server-specific libraries like
dart:io
, or e.g. access the filesystem, connect to a database etc. -
Client components must function and compile in both environments and therefore can neither directly import client- nor server-specific libraries. Instead, they either use shared implementations or have to use Darts conditional imports.
Because using conditional imports manually is pretty cumbersome, Jaspr provides the
@Import
utility to make working with platform-specific libraries in client components a lot easier.
- The import restrictions from (2) apply (obviously) also to all transitive imports of a component. One additional rule that follows that is that client components can/should never import server components.
Hydration Setup
For hydration to happen seamlessly the initial client-side render has to match the previously pre-rendered html from the server exactly. This is achieved by re-executing the same components that have previously pre-rendered the html on the server also on the client as soon as the browser loads the page.
With Jaspr you can handle hydration in two ways:
- manually by writing both server and client entrypoints separately, or
- automatically by using the
@client
annotation.
It is generally recommended to use automatic hydration.
Automatic Hydration (Recommended)
For automatic hydration simply use the @client
annotation on any component.
A component annotated with @client
will be automatically hydrated on the client after it has been pre-rendered. In principle,
this is like 'resuming' the rendering for a component on the client and picking up where the server-side rendering has left off.
Read more about @client
components and how to best use them here.
Manual Hydration
With manual hydration, you need to create a separate client entrypoint for your app inside the web/
directory:
// Client-specific import
import 'package:jaspr/browser.dart';
// Our main component
import 'lib/app.dart';
void main() {
// Attaches the app component to the <body> tag
// and hydrates the component / makes it interactive.
runApp(App(), attachTo: 'body');
}
This entrypoint, as any .dart
file, will then be compiled to javascript as <filename>.dart.js
.
To load this compiled javascript file on the client, include it as a <source>
element in your server-rendered html:
// Server-specific import
import 'package:jaspr/server.dart';
// Our main component
import 'app.dart';
import 'jaspr_options.dart';
void main() {
Jaspr.initializeApp(options: defaultJasprOptions);
runApp(Document(
head: [
// Links to the compiled `web/main.dart` file.
script(src: 'main.dart.js', []),
],
// Pre-renders the [App] component inside the <body> tag
body: App(),
));
}
The above setup mounts the App()
component directly inside the <body>
element. However, when you are building a more
content-heavy or mostly static website (static meaning without much user interaction)
you probably don't need to ship your whole app structure to the client, but rather want only certain parts of your
app to be interactive.
You can choose which part(s) of your app you want to hydrate by mounting only that part in your client entrypoint.
Additionally on the client, you can call runApp()
multiple times to mount different parts of your app separately:
Assuming you have a page layout like this:
<body>
<header>...</header>
<main>
<div id="content">...</div>
<div id="sidebar">...</div>
</main>
<footer>...</footer>
</body>
Your client entrypoint could be:
void main() {
runApp(Header(), attachTo: 'header');
runApp(Sidebar(), attachTo: '#sidebar');
runApp(Content(), attachTo: '#content');
}
This will run three apps simultaneously, attached to the specified root elements using css selectors.
The advantage of this approach is that you can leave other parts of your app, e.g. a static footer, out of the bundled javascript and thereby reducing loading and startup time.
Be aware that on the server, you must still construct the complete app layout and and render the targeted island components manually at the right location.