From d73a2470ea4b762012768a9e5c03a7f7a98282cb Mon Sep 17 00:00:00 2001 From: David Sancho Moreno Date: Thu, 6 Feb 2025 15:24:07 +0100 Subject: [PATCH] Create a basic usage page --- README.md | 2 +- demo/client/router.jsx | 4 +- documentation/basic-usage.mld | 82 ++++++++++++++++++++++ documentation/browser_only.mld | 87 ----------------------- documentation/browser_ppx.mld | 122 +++++++++++++++++++++++++++++++++ documentation/dune | 3 +- documentation/index.mld | 5 +- 7 files changed, 211 insertions(+), 94 deletions(-) create mode 100644 documentation/basic-usage.mld delete mode 100644 documentation/browser_only.mld create mode 100644 documentation/browser_ppx.mld diff --git a/README.md b/README.md index 8f3d9c099..bc60e654a 100755 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Aside from the core (`React`, `ReactDOM` and `ReactServerDOM`), server-reason-re | [`server-reason-react.melange_ppx`](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/externals-melange-attributes.html) | A ppx to add the melange attributes to native code | [melange.ppx](https://melange.re/v4.0.0/) | | `server-reason-react.promise` | Vendored version of [aantron/promise](https://github.com/aantron/promise) with melange support [PR#80](https://github.com/aantron/promise/pull/80) | [promise](https://github.com/aantron/promise) | | `server-reason-react.belt` | Implementation of Belt for native [API reference](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/server-reason-react.belt_native/Belt/index.html) | [melange.belt](https://melange.re/v4.0.0/api/ml/melange/Belt) | -| `server-reason-react.js` | Implementation of `Js` library for native (unsafe/incomplete) | [melange.js](https://melange.re/v4.0.0/api/ml/melange/Js) | +| `server-reason-react.js` | Implementation of `Js` library for native (unsafe/incomplete). Check the issue [#110](https://github.com/ml-in-barcelona/server-reason-react/issues/110) for more details | [melange.js](https://melange.re/v4.0.0/api/ml/melange/Js) | | `server-reason-react.fetch` | Stub of fetch with browser_ppx to compile in native | [melange.fetch](https://github.com/melange-community/melange-fetch) | | `server-reason-react.webapi` | Stub version of Webapi library for native code compilation | [melange-webapi](https://github.com/melange-community/melange-webapi) | | `server-reason-react.dom` | Stub version of Dom library for native code compilation | [melange-dom](https://melange.re/v4.0.0/) | diff --git a/demo/client/router.jsx b/demo/client/router.jsx index c215bfd7b..3f126feed 100644 --- a/demo/client/router.jsx +++ b/demo/client/router.jsx @@ -9,12 +9,10 @@ class ErrorBoundary extends React.Component { } static getDerivedStateFromError(error) { - // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { - // You can also log the error to an error reporting service console.error(error, errorInfo); } @@ -70,7 +68,7 @@ function App() { if (stream) { const element = document.getElementById("root"); - console.log("__client_manifest_map", window.__client_manifest_map); + /* console.log("__client_manifest_map", window.__client_manifest_map); */ React.startTransition(() => { ReactDOM.hydrateRoot(element, ); }); diff --git a/documentation/basic-usage.mld b/documentation/basic-usage.mld new file mode 100644 index 000000000..328042e27 --- /dev/null +++ b/documentation/basic-usage.mld @@ -0,0 +1,82 @@ +{0 Basic usage} + +server-reason-react use the same API as [reason-react] (explained in their {{:https://reasonml.github.io/reason-react/docs/en/components}official documentation}). + +Components are functions that return a [React.element] and are annotated with [@react.component]. By convention, they are named `make`, so when they are used inside JSX, they can be written without the `make` prefix and just the name of the module. If another name is used, it can be specified later in the JSX. For example, [] and [] are both valid. + +{[ +module Counter = { + [@react.component] + let make = (~name) => { + let (count, setCount) = React.useState(() => 0); + +
+

{React.string(name ++ " clicked " ++ Int.to_string(count) ++ " times")}

+ +
+ }; +}; + +module App = { + let make = () => { + + }; +}; +]} + +{[ +/* Imagine that this is a handler for an HTTP request, inside your server */ +let handler = (_request) => { + /* If we want to render the component to a string and return the HTML to the client: */ + let html = ReactDOM.renderToString(); + respond(html); +}; +]} + +Hooks like [React.useState] or [React.useEffect] are available in the [React] module, but when running in the server, they are no-ops. Since react components don't re-render on the server, hooks like [React.useCallback], [React.useMemo] have no memoization and return the same value just once. + +{1:render-to-string Server-side rendering with renderToString/renderToStaticMarkup} + +[ReactDOM.renderToString] renders a React tree to an HTML string. It does not support streaming or waiting for data. You can use [renderToStream] instead. + +{[ +let html = ReactDOM.renderToString(); +]} + +[ReactDOM.renderToStaticMarkup] renders a non-interactive React tree to an HTML string. Non-interactive means that the component should not be hydrated on the client. This can be useful to for Server-side generation of HTML (SSG). + +{[ +let html = ReactDOM.renderToStaticMarkup(); +]} + +{1:render-to-stream Server-side rendering with renderToStream} + +[ReactDOM.renderToStream] renders a React tree to a {{:https://ocsigen.org/lwt/3.1.0/api/Lwt_stream}Lwt_stream.t}. with type [Lwt_stream.t(string)]. + +This snippet assumes that you are using [Dream](https://dream.ocsigen.org/) to serve the HTML, and [lwt_ppx](https://github.com/ocsigen/lwt_ppx) to handle the [let%lwt] syntax. + +Those are optional choices, you can use any other server framework and not use [lwt_ppx]. +{[ +let%lwt (stream, abort) = ReactDOM.renderToStream(); +stream |> Lwt_stream.iter_s((chunk => { + /* Dream.write pushes the chunk into the response stream */ + let%lwt () = Dream.write(response_stream, chunk); + Dream.flush(response_stream); +})); + +/* abort is a function that can be called to stop the stream, and leaves the client to render the rest of the page */ +]} + +Note that [Lwt] is a required library. More details {{:https://github.com/ml-in-barcelona/server-reason-react/issues/205}in this issue}. + +{1:rsc React Server Components} + +React Server Components (RSC) is an architecture that allows you to render React components exclusively on the server, using Server-side code (such as Database queries or Filesystem operations). It allows to strip all client components (those components that require interactivity) from the JavaScript bundle sent to the client. + +There's a entire area of improvements that RSC bring to the table, such as lazy loading client components only when they are needed, remove fetching data with useEffect hooks (by passing promises to those client components), and removing state by lifting it to the URL. + +Half of the JavaScript ecosystem is losing their hair over this, but with [server-reason-react] you would be able to use from another language. + +This library supports it, but many pieces are being polished, check the {{:https://github.com/ml-in-barcelona/server-reason-react/tree/main/demo}demo folder} for more information or the {{:https://github.com/ml-in-barcelona/server-reason-react/issues/204}umbrella issue}. diff --git a/documentation/browser_only.mld b/documentation/browser_only.mld deleted file mode 100644 index b6f98a71c..000000000 --- a/documentation/browser_only.mld +++ /dev/null @@ -1,87 +0,0 @@ -{0 Exclude client code from the native build} - -[browser_only] is the ppx to exclude client code from the server build, making sure a library can run on both build targets. - -For example, if you're using Webapi to query the DOM or using LocalStorage. This code should only run on the client, and there's no equivalent or fallback on the server, in order to compile and run successfully on the server, you can use [browser_only] or [switch%platform]. - -{1 Usage} - -The ppx expose a [browser_only] attribute that can be used to discard a function or a value, and [switch%platform] to conditionally execute code based on the platform. - -Add [server-reason-react.browser_ppx] into to your pps field under a dune stanzas (melange.emit, libraries or executable) in your dune files. - -You would need to add it on both "server" and "client" dune files. Adding the [-js] flag [server-reason-react.browser_ppx -js] for the client and without for the server: -{[ -; server exectuable -(executable - (name server) - (preprocess - (pps server-reason-react.browser_ppx))) - -; melange emitting JavaScript -(melange.emit - (target app) - (preprocess - (pps server-reason-react.browser_ppx -js))) -]} - -{2 browser_only} -{[ -let%browser_only countDomNodes = (id) => { - let elements = Webapi.Element.querySelector("#" ++ id); - let arr_elements = Webapi.Element.toArray(elements); - Array.length(arr_elements); -}; -]} - -The method tagged by [browser_only] will keep the function untouched for the client build, but for the server build, will be transformed to a function that raises the exception [Runtime.Impossible_in_ssr]. - -If this function runs on the server, it will raise an exception, and the server will crash. In order to prevent this, you can use [try] to catch the exception and provide a default behaviour/value. - -Following with the example from above: -{[ -let%browser_only countDomNodes = (id) => { - let elements = Webapi.Element.querySelector("#" ++ id); - let arr_elements = Webapi.Element.toArray(elements); - Array.length(arr_elements); -} - -let main = id => - try(countDomNodes(id)) { - | _ => 0 - }; -]} - -{2 switch%platform} - -[switch%platform] allows to conditionally execute code based on the platform. There are some cases where you need to run a specific code only on the server or only on the client. - -{[ -switch%platform (Runtime.platform) { -| Server => print_endline("This prints to the terminal") -| Client => Js.log("This prints to the console") -}; -]} - -{2 @platform attribute} - -The [@platform] attribute allows to specify code blocks that should only be included in the JavaScript or native build, respectively. This is useful when you have code that is specific to one platform and should not be included in the other. - -For example, you can define two modules, but only one of them should be kept in the final build based on the platform. -{[ -[@platform js] -module X = { - type t = Js.Json.t; - - let a = 2 + 2; -}; - -[@platform native] -module Y = { - type t = Js.Json.t; - - let a = 4 + 4; -}; -]} - -When compiling with the `-js` flag, only the block with [[@platform js]] (module X) is kept, and when compiling without it, only the block with [[@platform native]] (module Y) is kept. diff --git a/documentation/browser_ppx.mld b/documentation/browser_ppx.mld new file mode 100644 index 000000000..43d0fd19e --- /dev/null +++ b/documentation/browser_ppx.mld @@ -0,0 +1,122 @@ +{0 Exclude client code from the native build} + +[browser_only] is the ppx to exclude client code from the server build, making sure a library can run on both build targets. It also allows to conditionally execute code based on the platform. + +For example, if you're using Webapi to query the DOM and extract some data from it. This code should only run on the client, and there's no equivalent or fallback on the server. + +In order to compile and run successfully on the server, you can use [let%browser_only],[switch%platform] or the [@platform]. This page explains how to use them. + +{1 Example} + +The ppx expose a [%browser_only] attribute that can be used to discard a function, and [switch%platform] to conditionally execute code based on the platform. + +{[ +let%browser_only countDomNodes = (id) => { + let elements = Webapi.Element.querySelector("#" ++ id); + let arr_elements = Webapi.Element.toArray(elements); + Array.length(arr_elements); +} +]} + +{[ +switch%platform (Runtime.platform) { +| Server => print_endline("This prints to the terminal") +| Client => Js.log("This prints to the console") +}; +]} + +{1 Installation} + +Add [server-reason-react.browser_ppx] into to your pps field under a dune stanzas (melange.emit, libraries or executable) in your dune files. + +You would need to add it on both "server" and "client" dune files. Adding the [-js] flag [server-reason-react.browser_ppx -js] for the client and without for the server: +{[ +; server exectuable +(executable + (name server) + (preprocess + (pps server-reason-react.browser_ppx))) + +; melange emitting JavaScript +(melange.emit + (target app) + (preprocess + (pps server-reason-react.browser_ppx -js))) +]} + +{1 Usage} + +{2 let%browser_only to discard functions} +{[ +let%browser_only countDomNodes = (id) => { + let elements = Webapi.Element.querySelector("#" ++ id); + let arr_elements = Webapi.Element.toArray(elements); + Array.length(arr_elements); +}; +]} + +The method tagged by [browser_only] will keep the function for the client build, but will be discarded for the server build. In more detail, the body of function will be transformed to a [Runtime.Impossible_in_ssr] exception (and + +If this function ever runs on the server by accident, it will raise the exception. If this exception isn't caught, the server will crash. Calling a function on the server that should only run on the client, it's very unlikely to happen. + +In any case, there may be cases where catch the exception and provide a default behaviour/value, can be useful. + +Following with the example from above: +{[ +let%browser_only countDomNodes = (id) => { + let elements = Webapi.Element.querySelector("#" ++ id); + let arr_elements = Webapi.Element.toArray(elements); + Array.length(arr_elements); +} + +let main = id => + switch (countDomNodes(id)) { + | exception Runtime.Impossible_in_ssr(_message) => 0 + }; +]} + +{2 switch%platform to conditionally exclude an expression for each platform} + +[switch%platform] allows to conditionally execute code based on the platform. There are some cases where you need to run a specific code only on the server or only on the client. + +{[ +switch%platform (Runtime.platform) { +| Server => print_endline("This prints to the terminal") +| Client => Js.log("This prints to the console") +}; +]} + +Not only executing code, but any expression can be part of the switch. + +{[ +let howManyColumns = + switch%platform (Runtime.platform) { + | Server => 0 + | Client => 12 + }; +]} + +Note that the expression is evaluated for each platform, but the type needs to be the same for all the branches + +{2 @platform attribute} + +The [@platform] attribute allows to specify code blocks that should only be included in the JavaScript or native build, respectively. This is useful when you have code that is specific to one platform and should not be included in the other. + +For example, you can define two modules, but only one of them should be kept in the final build based on the platform. +{[ +[@platform js] +module X = { + type t = Js.Json.t; + + let a = 2 + 2; +}; + +[@platform native] +module Y = { + type t = Js.Json.t; + + let a = 4 + 4; +}; +]} + +When compiling with the `-js` flag, only the block with [[@platform js]] (module X) is kept, and when compiling without it, only the block with [[@platform native]] (module Y) is kept. If you name the modules the same, the compiler won't complain and you would get a single module available in both targets. diff --git a/documentation/dune b/documentation/dune index 88baac7a0..c71a74904 100644 --- a/documentation/dune +++ b/documentation/dune @@ -3,7 +3,8 @@ (mld_files index get-started + basic-usage universal-code how-to-organise-universal-code - browser_only + browser_ppx externals-melange-attributes)) diff --git a/documentation/index.mld b/documentation/index.mld index d3760aeb5..ef92e743a 100644 --- a/documentation/index.mld +++ b/documentation/index.mld @@ -2,15 +2,16 @@ This site is the documentation page and API references for {{:https://github.com/ml-in-barcelona/server-reason-react}server-reason-react}, server-reason-react is a Native implementation of React's server-side rendering (SSR) and React Server Components (RSC) architecture for {{:https://reasonml.github.io/}Reason}. -Designed to be used with {{:https://reasonml.github.io/reason-react//}reason-react} and {{:https://github.com/melange-re/melange}Melange}. Together it enables developers to write efficient React components using a single language, while target both native executable and JavaScript. +Designed to be used with {{:https://reasonml.github.io/reason-react//}reason-react} and {{:https://github.com/melange-re/melange}Melange}. Together it enables developers to write efficient React components using a single language, while building for both native executables and JavaScript. {1:guides Guides} {ol {li {{!page-"get-started"}Get started}} + {li {{!page-"basic-usage"}Basic usage}} {li {{!page-"universal-code"}What does universal code mean?}} {li {{!page-"how-to-organise-universal-code"}How to organise universal code}} - {li {{!page-"browser_only"}Exclude client code from the native build}} + {li {{!page-"browser_ppx"}Exclude client code from the native build}} {li {{!page-"externals-melange-attributes"}Externals and melange attributes}} }