LogoJaspr

๐Ÿ— Server Side Rendering

Jaspr is built to be server-side-rendering first. It leverages the power of Darts cross-compilation to both js and native executables to execute the same app on the server and client.

For each target (server and client) you need to have a separate entrypoint to start your app. However, depending on what project setup you choose, the web entrypoint might be automatically generated for you.

  1. The entrypoint on the web will always be the corresponding file inside the web directory, usually web/main.dart. This file will be compiled to js and is included in the index.html file as a script: <script defer src="main.dart.js"></script>

  2. The entrypoint on the server will be lib/main.dart or the file specified using the -i (--input) option when running jaspr serve or jaspr build.

๐Ÿ’ฝ Loading Data on the Server#

When using server side rendering, you want the ability to load some data asychrously before rendering the components. With jaspr this is build into the framework and easy to do.

Start by using the PreloadStateMixin on a StatefulComponents State class and implement the Future<T> preloadState() method.

class MyState extends State<MyStatefulComponent> with PreloadStateMixin {
  
  @override
  Future<void> preloadState() async {
    ...
  }
}

This method will only be executed on the server and will be called before initState(). It will defer initState() as well as building the component until the future returned by the method is completed.

While it is only executed on the server it still will be compiled for the client, since Dart does not have selective compilation. So you have to make sure it will compile on the client, e.g. by using service locators and providing mock services on the client. (TODO: More explanation and How To)

This is a great use-case for jasprs platform-specific imports

โ™ป๏ธ Syncing Data to the Client#

Alongside with preloading some data on the server, you often want to also sync the data with the client. You can simply add the SyncStateMixin which will automatically sync state from the server with the client.

The SyncStateMixin accepts a second type argument for the data type that you want to sync. You then have to implement the getState and updateState() methods.

class MyState extends State<MyStatefulComponent> with SyncStateMixin<MyStatefulComponent, MyStateModel> {

  MyStateModel? model;
  
  // a globally unique id that is used to identify the state
  @override
  String get syncId => 'my_id';

  // this will get the state to be sent to the client
  // and is only executed on the server
  @override
  MyStateModel? getState() {
    return model;
  }

  // this will receive the state on the client
  // and it is safe to call setState
  void updateState(MyStateModel? value) {
    setState(() {
      model = value;
    });
  }
}

In order to send the data, it needs to be serialized on the server and deserialized on the client. The serialization format is defined by the syncCodec getter defined in SyncStateMixin.

The codec must encode to a primitive or object value of one of the supported types: Null, bool, double, int, Uint8List, String, Map, List. By default, the codec is null and your state must already be one of the supported types.

When you want to use another type like a custom model class, you have to override the syncCodec getter, which has to return a Codec that encodes to String. This can be any codec, however a typical use would be to encode your model to Map<String, dynamic> and the fuse this with the json codec to get a json string.


class MyState extends State<MyStatefulComponent> with SyncStateMixin<MyStatefulComponent, MyStateModel> {

  @override
  Codec<MyStateModel, String> get syncCodec => MyStateModelCodec();

  ...
}

// codec that encodes a value of MyStateModel to Map<String, dynamic>
class MyStateModelCodec extends Codec<MyStateModel, Map<String, dynamic>> {
  
  ...
  
}