@client Components
With the @client annotation you can automatically hydrate selected components on the client.
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 and picking up where the server-side rendering has left off.
A @client
component acts as a 'boundary' between server and client. Components in the tree above will be rendered only on
the server, while components in the tree below will be rendered both on the server and client.
It is generally recommended to keep a 'mental note' about which components are only rendered on the server, and which are also rendered on the client. This distinction may become important later when you develop your website and want to import some server-specific or client-specific library or package. Then you need to make sure that your component compiles for all environments it will be rendered in.
Usage
For @client
to work, make sure to call Jaspr.initializeApp(options: defaultJasprOptions);
before runApp();
in your main.dart
(This is already setup when creating a new project).
Then simply annotate your desired component with @client
like this:
import 'package:jaspr/jaspr.dart';
// Turns this into a client component.
@client
class App extends StatelessComponent {
const App({super.key});
@override
Iterable<Component> build(BuildContext context) sync* {
yield /* ... */;
}
}
Only one @client
component per file is allowed.
You can use @client
components normally as any other component. You can also have multiple components annotated with
@client
to have separate interactive parts on the client. However, nesting client components is currently not possible.
Initializing client logic
When using @client
components there is no default main()
function on the client where you would normally
run any client-side initialization logic. Instead, you can make your @client
component stateful and use the initState()
method for calling your initialization logic:
@override
void initState() {
super.initState();
// Optionally skip the initialization logic on the server.
if (kIsWeb) {
// Your initialization logic, e.g. for setting up service locators, plugins or other global state.
}
}
Sharing state
When you use multiple @client
components, each one will be mounted on the client in a separate component tree as the root.
Therefore if you want to share state across different @client
components on the client, you cannot use any tree-based
state solutions like InheritedComponent
s. Instead, you can create a global state container outside of the tree and inject
it for each @client
component:
The following is just an example for an implementation using ChangeNotifier
and InheritedComponent
.
You can also use other implementations for the container and the tree injection.
// Some class that holds the shared state. Can be a ChangeNotifier or some other state primitive.
class StateContainer extends ChangeNotifier {
// Your state
State myState;
}
// Global state container to be injected into each tree.
final _container = StateContainer();
class InheritedStateContainer extends InheritedComponent {
const InheritedStateContainer({required this.container, super.child, super.key});
final StateContainer container;
}
@client
class MyClientComponent extends StatelessComponent {
@override
Iterable<Component> build(context) sync* {
// Inject the shared container at the root of each client component tree.
yield InheritedStateContainer(
container: _container,
child: /* ... */,
);
}
}
Many state-management solutions also have some support for this. The same concept using jaspr_riverpod
would
look like this:
final _container = ProviderContainer();
@client
class MyClientComponent extends StatelessComponent {
@override
Iterable<Component> build(context) sync* {
// Use the shared container for the provider scope.
yield ProviderScope(
container: _container,
child: /* ... */,
);
}
}
Passing data
As any other component, @client
components can have parameters, with certain limitations. Parameters are automatically
serialized on the server and de-serialized on the client when hydrating the component. Therefore, using @client
components
with parameters are a great way to pass data from the server to the client. For this to work, the following requirements apply:
1. All parameters must be initializing field parameters (this.<fieldname>
):
@client
class App extends StatelessComponent {
const App({required this.title, super.key});
final String title;
/* ... */
}
2. All parameters must be serializable:
Parameters must either have a primitive serializable type: bool
, int
, double
, String
or List
/ Map
s of these.
Or you can use custom data types by using the @encoder
and @decoder
annotations with the class:
class Model {
@decoder
static Model fromJson(Map<String, dynamic> json) => /* ... */;
@encoder
Map<String, dynamic> toJson() => /* ... */;
}
Learn more about how to set up serialization for custom data types using the @encoder
/ @decoder
annotations.
With this setup you can use any class as the parameter of a @client
component.
How it works
The following happens when you use a @client
component:
during build:
- The component (along with some framework bits) is compiled to a separate js entrypoint.
on the server:
- The component is built and pre-rendered normally.
- Jaspr adds a html marker (
<!--$<name> data=<serialized-parameters>-->
) around your components output. - Jaspr adds the components js target as a
<script>
tag to the documents<head>
.
on the client:
- The browser loads the pre-rendered html and compiled js scripts.
- The used component is located based on the html marker.
- The parameters are deserialized and the component is mounted to the target element.