Client-Side Jaspr

Learn about client-side rendering and hydration in Jaspr.

This page is relevant for all modes.

The client environment is where all interactivity, event handling, and access to browser APIs happens. In client mode, your app is entirely executed on the client, while in server and static mode, the client-side rendering is started after the initial server-side rendering.

When using static or server mode, make sure to also read the Server-Side Jaspr page to understand the difference between server and client environments.

You can choose to compile the client-side part of your app to either JavaScript (default) or WebAssembly (by using the --experimental-wasm flag when running jaspr serve or jaspr build).

Client Entrypoint

By default, your application's client entrypoint is lib/main.client.dart. This file is automatically used by the Jaspr when serving or building your application.

In your entrypoint, you should:

  • import package:jaspr/client.dart and the generated <filename>.client.options.dart,
  • call Jaspr.initializeAll(options: defaultClientOptions); and
  • call the runApp() method, which will start the Hydration process described below.
lib/main.client.dart
// Client-specific Jaspr import.
import 'package:jaspr/client.dart';

// This file is generated automatically by Jaspr, do not remove or edit.
import 'main.client.options.dart';

void main() {
  // Initializes the client environment with the generated default options.
  Jaspr.initializeAll(options: defaultClientOptions);

  // Starts running the app.
  runApp(
    ...
  );
}

The root component depends on your rendering mode:

runApp(
  ClientApp(),
);

The ClientApp component automatically loads and renders all components annotated with @client that have been pre-rendered on the server.

You can wrap this component with additional components such as an InheritedComponent to share state across multiple @client components.

Avoid wrapping ClientApp with any HTML components, as this may break the hydration process.

Multiple Entrypoints

You can have multiple entrypoints, e.g. for different flavors, by following the <filename>.client.dart naming convention (doesn't have to be in root of lib). All files matching this pattern will be compiled to either JavaScript or WebAssembly (depending on the --experimental-wasm flag) and form a separate client bundle. You can then specify which bundle to use depending on the rendering mode:

If you have a peer <filename>.server.dart file next to your <filename>.client.dart entrypoint, the client bundle is automatically included during the server-side rendering.

For standalone client entrypoints, you need to manually include a script tag pointing to <filename>.client.dart.js in your page (usually in Document(head: [...])):

script(src: 'somefile.client.dart.js', defer: true),

The src path is defined relative to the lib/ directory and may point to a file in a sub-directory.

Hydration

Hydration is the process of making a web page interactive by attaching event handlers to the existing html during the initial build phase.

How Jaspr handles hydration depends on the rendering 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.

To optimize the initial load time, you can selectively hydrate only specific components, instead of your entire app. This is done by using the @client annotation on components you want to hydrate.

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.

@client components also have other features such as passing data from server to client or hydrating components dynamically based on server-side state.

Read more about @client components and how to best use them here.

For @client components to hydrate, all you have to do is provide the ClientApp() component to your runApp() call.

Although it is not recommended, you can choose to not use @client components and instead setup hydration manually. You will loose many of the benefits of using @client components, such as passing data from server to client or hydrating components dynamically based on server-side state. If you choose to do so, follow the setup described in the Client Mode tab.

Adding Interactivity

Interactivity in Jaspr is handled through standard HTML events and either StatefulComponents or other state management solutions.

All HTML components in Jaspr accept an events parameter to listen to DOM events. Some HTML components, like button, input, etc. also have special event handler parameters, such as onClick, onChange, etc.

// Using the onClick parameter.
button(
  onClick: () {
    print('Button clicked');
  },
  [.text('Click me')]
)

// Using the standard events parameter.
div(
  events: {'click': (web.Event event) {
    print('Button clicked');
  }},
  [.text('Click me')]
)

There is also a events() helper function that can be used to create event callbacks with type safety on any component.

Working with the DOM

The DOM (Document Object Model) is the browser's representation of the page's structure. It is a tree of nodes that represent the HTML elements of the page.

While Jaspr handles most DOM updates for you, sometimes you need direct access to the underlying DOM elements or browser APIs.

Accessing Elements

To access the DOM node of a specific component, you can use a GlobalNodeKey. Pass this key to the component you want to access, and use key.currentNode to get the element.

import 'package:universal_web/web.dart' as web;

final GlobalNodeKey<web.HTMLInputElement> inputKey = GlobalNodeKey();

// In your build method:
input(key: inputKey, []);

// Later, e.g. in a button click handler:
void focusInput() {
  inputKey.currentNode?.focus();
}

Browser APIs

The recommended way to access browser APIs depends on your rendering mode:

To access browser APIs like window or document, use package:universal_web. This package provides a consistent API across both server (where it mocks the APIs) and client environments.

Make sure to wrap access to browser APIs in a kIsWeb check, otherwise you will get an exception during server-side rendering.

import 'package:universal_web/web.dart' as web;

void logSize() {
  if (kIsWeb) {
    print('Window size: ${web.window.innerWidth}x${web.window.innerHeight}');
  }
}

Global Events

To listen to global events like window.resize or document.scroll, you can use EventStreamProviders. These provide a typed Stream of events.

import 'package:universal_web/web.dart' as web;

// In your State class:
StreamSubscription? sub;

@override
void initState() {
  super.initState();
  
  if (kIsWeb) {
    sub = web.EventStreamProviders.resizeEvent.forTarget(web.window).listen((e) {
      print('Window resized');
    });
  }
}

@override
void dispose() {
  sub?.cancel();
  super.dispose();
}

Using js_interop

Since Jaspr compiles to JavaScript (or WebAssembly), you can easily interact with existing JavaScript libraries or snippets through Dart's js_interop mechanism.

Use package:universal_web/js_interop to safely import Dart's js_interop library across server (where it mocks the APIs) and client environments.

import 'package:universal_web/js_interop.dart';

Use @JS() to access top-level JavaScript functions or variables.

@JS()
external void alert(JSString message);

void showAlert() {
  alert('Hello from Dart!'.toJS);
}

Use extension types to access members of JavaScript objects.

extension type Console._(JSObject _) implements JSObject {
  external void log(JSAny? item);
}

@JS()
external Console get console;

void logToConsole() {
  console.log('Hello from Dart!'.toJS);
}

For standard web APIs, always prefer using the package:web or package:universal_web package. Use js_interop only for external libraries or advanced use cases.

For more complex interactions or using external libraries, refer to the official Dart documentation on JS interop.