Skip to main content
Client-side rendering in Jaspr allows you to create interactive components that run in the browser. Using the @client annotation, you can mark specific components for client-side hydration while keeping the rest of your app server-rendered.

The @client Annotation

Mark any component with @client to make it interactive in the browser:
import 'package:jaspr/jaspr.dart';

@client
class Counter extends StatefulComponent {
  const Counter();
  
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;
  
  @override
  Component build(BuildContext context) {
    return div([
      p([.text('Count: $count')]),
      button(
        onClick: () => setState(() => count++),
        [.text('Increment')],
      ),
    ]);
  }
}
Client components are server-rendered first (for SEO and fast initial load), then hydrated in the browser to add interactivity.

How Hydration Works

Jaspr’s hydration process follows these steps:
1

Server Renders Component

The server pre-renders the client component to HTML, including special markers:
<!--$:Counter:eyJpbml0aWFsQ291bnQiOjB9-->
<div>
  <p>Count: 0</p>
  <button>Increment</button>
</div>
<!--/$:Counter-->
2

Browser Loads Page

The HTML is displayed immediately - users see content before JavaScript loads.
3

ClientApp Initializes

The ClientApp component scans the DOM for client component markers.
4

Components Hydrate

Each client component is mounted and “hydrated” - the existing DOM is reused and event listeners are attached.

Client Entry Point

Create a client entry point (typically lib/main.client.dart):
lib/main.client.dart
import 'package:jaspr/client.dart';

void main() {
  Jaspr.initializeApp(
    options: defaultClientOptions,
  );
  
  runApp(ClientApp());
}

ClientApp Component

The ClientApp component is required for hydrating client components:
import 'package:jaspr/client.dart';

runApp(ClientApp());
Source: packages/jaspr/lib/src/client/client_app.dart:23 This component:
  • Locates all @client components in the DOM
  • Initializes them with their parameters
  • Manages their lifecycle
You must wrap your root app with ClientApp() for client components to hydrate properly.

Client Configuration

The Jaspr.initializeApp() method on the client accepts options:
Jaspr.initializeApp(
  options: ClientOptions(
    // Optional initialization callback
    initialize: () {
      print('Client app initialized');
    },
    
    // Map of client component names to loaders
    clients: {
      'Counter': ClientLoader(
        (params) => Counter(),
      ),
      'TodoList': ClientLoader(
        (params) => TodoList(items: params['items']),
        loader: () async {
          // Lazy-load dependencies
          await loadTodoListDependencies();
        },
      ),
    },
  ),
);
In practice, use the auto-generated defaultClientOptions instead of manually creating client options.

ClientOptions

Source: packages/jaspr/lib/src/client/options.dart:26
class ClientOptions {
  const ClientOptions({
    this.initialize,
    this.clients = const {},
  });
  
  final void Function()? initialize;
  final Map<String, ClientLoader> clients;
}

ClientLoader

Source: packages/jaspr/lib/src/client/options.dart:37 Loaders handle async initialization and lazy loading:
class ClientLoader {
  ClientLoader(this.builder, {this.loader});
  
  final Future<void> Function()? loader;
  final ClientBuilder builder;
}

typedef ClientBuilder = Component Function(Map<String, dynamic> params);

Passing Parameters

Pass data from server to client components:
@client
class UserCard extends StatelessComponent {
  const UserCard({required this.name, required this.email});
  
  final String name;
  final String email;
  
  @override
  Component build(BuildContext context) {
    return div([
      h3([.text(name)]),
      p([.text(email)]),
    ]);
  }
}

// Usage in server component:
UserCard(name: 'Alice', email: '[email protected]')
Parameters are JSON-encoded and embedded in the HTML, then decoded on the client.

Client-Only Features

Browser APIs

Access browser APIs safely:
import 'package:jaspr/jaspr.dart';

@Import.onWeb('dart:html', show: [#window, #document])
import 'my_component.imports.dart';

@client
class BrowserInfo extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    String userAgent = '';
    
    if (kIsWeb) {
      userAgent = window.navigator.userAgent;
    }
    
    return div([
      p([.text('User Agent: $userAgent')]),
    ]);
  }
}

Event Handlers

Attach event handlers to interactive elements:
@client
class ClickCounter extends StatefulComponent {
  @override
  State createState() => _ClickCounterState();
}

class _ClickCounterState extends State<ClickCounter> {
  int clicks = 0;
  
  @override
  Component build(BuildContext context) {
    return button(
      onClick: () {
        setState(() => clicks++);
        print('Button clicked $clicks times');
      },
      [.text('Clicks: $clicks')],
    );
  }
}

Streams and Futures

Use async data sources in client components:
@client
class RealtimeCounter extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return StreamBuilder(
      stream: Stream.periodic(
        Duration(seconds: 1),
        (count) => count,
      ),
      builder: (context, snapshot) {
        return div([
          .text('Count: ${snapshot.data ?? 0}'),
        ]);
      },
    );
  }
}
When using StreamBuilder or FutureBuilder on the server, pass null for the stream/future to avoid server-side execution. Use kIsWeb to conditionally provide the stream/future:
StreamBuilder(
  stream: kIsWeb ? myStream : null,
  builder: (context, snapshot) { ... },
)

Lifecycle Hooks

Client components support standard Jaspr lifecycle methods:
@client
class LifecycleExample extends StatefulComponent {
  @override
  State createState() => _LifecycleExampleState();
}

class _LifecycleExampleState extends State<LifecycleExample> {
  @override
  void initState() {
    super.initState();
    print('Component initialized');
  }
  
  @override
  void didMount() {
    super.didMount();
    print('Component mounted to DOM');
  }
  
  @override
  void didUpdateComponent(LifecycleExample oldComponent) {
    super.didUpdateComponent(oldComponent);
    print('Component updated');
  }
  
  @override
  void dispose() {
    print('Component disposed');
    super.dispose();
  }
  
  @override
  Component build(BuildContext context) {
    return div([.text('Lifecycle example')]);
  }
}

Optimization

Lazy Loading

Lazy-load client component code:
ClientLoader(
  (params) => HeavyComponent(),
  loader: () async {
    // Load dependencies before initializing
    await Future.delayed(Duration(milliseconds: 100));
    print('Heavy component dependencies loaded');
  },
)

Conditional Client Components

Only hydrate components when needed:
class ConditionalInteractive extends StatelessComponent {
  const ConditionalInteractive({required this.isInteractive});
  
  final bool isInteractive;
  
  @override
  Component build(BuildContext context) {
    if (isInteractive) {
      return InteractiveButton();
    }
    return StaticButton();
  }
}

@client
class InteractiveButton extends StatelessComponent {
  // Only this component loads client-side code
}

class StaticButton extends StatelessComponent {
  // No client-side code needed
}

API Reference

runApp()

Source: packages/jaspr/lib/src/client/run_app.dart:5
ClientAppBinding runApp(Component app, {String attachTo = 'body'})
Attaches the client app to the DOM. The attachTo parameter specifies the CSS selector to attach to.

ClientAppBinding

Source: packages/jaspr/lib/src/client/client_binding.dart:12 The binding that manages client-side component rendering and DOM updates.

Best Practices

Minimize Client Components

Only mark components as @client when they need interactivity. Keep as much as possible server-rendered for better SEO and performance.

Pass Minimal Data

Only pass the data needed for initial render. Fetch additional data on the client if needed.

Handle Loading States

Always handle loading and error states in async components.

Use kIsWeb Guard

Guard browser-specific code with if (kIsWeb) to prevent server-side errors.

Next Steps

Server-Side Rendering

Learn about server-side rendering and async components

Static Site Generation

Pre-render pages with client-side interactivity

Build docs developers (and LLMs) love