package server-reason-react

  1. Overview
  2. Docs
Legend:
Library
Module
Module type
Parameter
Class
Class type

How to organise the universal code

While using server-reason-react it's important to know how to organise the code. Sometimes you may want to have components that are shared between the client and the server, and sometimes you want to have components that are only used by the client or the server.

In order to have universal code, we need to organise the code in a way that allows us to do so. In this guide, we will asume you are using Melange and dune.

Pure universal library

If there's a library without any client or server dependency, you can just have a library with all modes: (modes native byte melange). This is common for type-only libraries or libraries that only rely on the standard library. I often refer to this as "pure universal" library.

For example, a tiny library to handle remote data called Remote_data:

(library
 (name remote_data)
 (modes native melange))
type t('data, 'error) =
  | NotAsked
  | InitialLoading
  | Loading('data)
  | Failure('error)
  | Success('data);

let map = (remoteData, fn) =>
  switch (remoteData) {
  | NotAsked => NotAsked
  | InitialLoading
  | Loading(_) => InitialLoading
  | Failure(error) => Failure(error)
  | Success(data) => Success(fn(data))
  };

let getWithDefault = (remoteData, defaultValue) =>
  switch (remoteData) {
  | NotAsked
  | InitialLoading
  | Loading(_)
  | Failure(_) => defaultValue
  | Success(data) => data
  };

let isLoading =
  fun
  | InitialLoading
  | Loading(_) => true
  | _ => false;

This is a cut down version of the library for demonstration purposes, imagine here to have all sort of functions to operate on the type.

Same API, different implementation

There are some other cases where you want to expose the same API, but the implementation is different. For example, another tiny example: you may want to have a library that exposes a function to get the current time. On the client, you may want to use the browser API, while on the server you may want to use the system time.

dune allows to have 2 libraries with the same name, but availalbe in different modes. For example:

(library
  (name url_js)
  (modes melange)
  (libraries melange.js)
  (wrapped false)
  (modules Url)
  (preprocess (pps melange.ppx))

(library
  (name url_native)
  (modes native)
  (modules Url)
  (wrapped false))

Both libraries need to be (wrapped false) so are exposed under the same name. wrapped false means that the library is not wrapped in a entry module, so the modules are exposed directly. In this case a `Url.re` module should exist on both libraries.

copy_files

In order to reuse the same code, you can use (copy_files ...). It seems hacky, and eventually we will have better ways of doing so, but is the method I found to be more reliable in terms of developer experience, mostly editor support and error messages.

- src
  - client/
    - dune
  - server/
    - shared/
        <library-code-here>
    - dune
(* src/client/dune *)

(library
  (name url_js)
  (modes melange)
  (libraries melange.js)
  (wrapped false)
  (modules Url)
  (preprocess (pps melange.ppx))

(copy_files#
 (mode fallback) ; `mode fallback` means you can override files in the client folder
 (files "../native/shared/**.{re,rei}"))
(* src/server/dune *)

(library
  (name url_native)
  (modes native)
  (modules Url)
  (wrapped false))

Here's an example https://github.com/ml-in-barcelona/server-reason-react/tree/main/demo/universal

reason-react and server-reason-react

Asuming you want to share react.components between the client and the server, you can use the same technique as above.

(library
 (name shared_js)
 (modes melange)
 (libraries reason_react melange.belt bs_webapi)
 (wrapped false)
 (preprocess
  (pps melange.ppx reason-react-ppx)))

(copy_files# "../native/lib/*.re")

(library
 (name shared_native)
 (modes native)
 (libraries
  server-reason-react.react
  server-reason-react.reactDom
  server-reason-react.belt
  server-reason-react.webapi)
 (wrapped false)
 (preprocess
  (pps
    server-reason-react.ppx
    server-reason-react.browser_ppx
    server-reason-react.melange_ppx)))

(copy_files# "../*.re")

This will expose all modules under a `Shared` module. You can then use those modules in both the client and the server.

// client.re

switch (ReactDOM.querySelector("#root")) {
| Some(el) =>
  let root = ReactDOM.Client.hydrateRoot(el);
  ReactDOM.Client.hydrate(<Shared.App />, root);
| None => Js.log("Can't find a 'root' element")
};
// server.re
// Given a random server library, and a random Page component

module Page = {
  [@react.component]
  let make = (~children, ~scripts) => {
    <html>
      <head>
        <meta charSet="UTF-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1.0"
        />
        <title> {React.string("Server Reason React demo")} </title>
        <link
          rel="shortcut icon"
          href="https://reasonml.github.io/img/icon_50.png"
        />
        <script src="https://cdn.tailwindcss.com" />
      </head>
      <body> <div id="root"> children </div> </body>
    </html>;
  };
};

// ...
req => {
  let html = ReactDOM.renderToString(<Page> <Shared.App /> </Page>);
  Httpd.Response.make_string(Ok(html));
}

Note on virtual_libraries

There's a better mechanismo of doing the same thing by dune, which is Virtual libraries.

However, there are a few limitations on virtual libraries:

I found that this mechanism is not as reliable as copy_files, and it's not well supported by editors. I would recommend to use copy_files instead, while we explore better ways of doing so with the dune team.

Next

  1. How universal code works
OCaml

Innovation. Community. Security.