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 this guide, we will asume you are using Melange and dune, and will show a few examples of universal code and how to setup the dune files accordingly.
Pure universal library
It'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.
A tiny library to handle remote data named Remote_data
:
(library
(name remote_data)
(modes native melange)) ; Contains both modes for melange and native
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, you can imagine to have all necessary functions to operate on this type.
This library can be used in both "native" and "melagne" stanzas interchangeably.
Same API, different implementations
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 available in different modes. For example:
(library
(name url_js)
(modes melange)
(libraries melange.js)
(modules Url)
(wrapped false))
(library
(name url_native)
(modes native)
(modules Url)
(wrapped false))
url_js
and url_native
are two different libraries, but they expose the same module called Url
with the same API.
Both libraries need to be (wrapped false)
so they expose all the modules (which in this case is only Url
) directly.
wrapped true
means that the library is wrapped in a entry module, so the modules are exposed under the library name. In this case, wrapped false
expose the modules directly.
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
- Exclude client code from the native build
- Externals and melange attributes