templates with holes, compiled
Your reason‑react code, without React.
Change two lines in your dune files. Melact keeps reason-react's surface and compiles your JSX into DOM templates with holes. A re-render writes only the holes whose values changed.
This is the real Melact build. Each click writes only what changed.
Click + to see what it writes.
- lines to change in your dune files
- 2
- libraries and preprocess
- npm runtime dependencies
- 0
- the runtime is compiled OCaml
- steps of random walks through reason-seoul's TodoMVC with the same DOM as React 18
- 605/605
- only its dune file changed
- steps of reason-react's own demo with the same DOM as React 18
- 37/37
- compared after every click
Migrate in two lines
Change the libraries. Keep the components.
React.useState, useEffect, useContext, ReactDOM.Client,
React.Event, ReasonReactRouter …) and replaces what is underneath it.
1your dune file
- (libraries reason-react)
+ (libraries melact)
- (preprocess (pps reason-react-ppx))
+ (preprocess (pps melact-ppx))2your components stay as they are
let[@react.component] make ~initial () =
let count, setCount = React.useState (fun () -> initial) in
let tone = if count < 0 then "negative" else "" in
<div className="counter">
<button onClick=(fun _ -> setCount (fun n -> n - 1))>(React.string "-")</button>
<output className=tone>(React.int count)</output>
<button onClick=(fun _ -> setCount (fun n -> n + 1))>(React.string "+")</button>
<button onClick=(fun _ -> setCount (fun _ -> initial)) disabled=(count = initial)>
(React.string "reset")
</button>
</div>In one picture
Your code stays. What runs underneath changes.
Compatibility
Checked against React 18, not assumed.
dual test suite
One suite, two Reacts
One suite in mlx is built twice, against reason-react with React 18 and against Melact, with identical assertions.
compat/run.sh
Real apps, two lines changed
Clones reason-react, melange-opam-template and reason-seoul's TodoMVC, changes two lines in their dune files and builds them against Melact, then compares the DOM with React's after every step.
laws
Properties, not scenarios
144 vitest + jsdom tests and 25 native QCheck laws. One law sends random mouse and keyboard events that handlers prevent or stop, and React 18 and Melact must read the same values.
reason-react surface that works today
- useState
- useReducer
- useEffect*
- useLayoutEffect*
- useMemo*
- useCallback*
- useRef
- useImperativeHandle*
- useId
- useContext
- React.memo
- React.act
- React.Event
- React.createElement + makeProps
- ReactDOM.Client
- ReactDOM.Style
- ReactDOM.Ref
- ReactDOMServer.renderToString
- ReactDOM.Client.hydrateRoot
- ReasonReactRouter
Why Melact
The same component model, compiled.
- 01
No virtual DOM
A render does not build a tree of elements to diff. It builds one array: the values of the holes in a template. Everything static is parsed once, at module load, and cloned.
- 02
Exact change detection
“Did this hole change” is ===. An unchanged reference is an unchanged value, so a physically equal child list or lazy argument skips its whole subtree.
- 03
Updates start where state changed
A setState re-renders its own component, not the root. Children with physically unchanged props are skipped without memo.
- 04
Few DOM moves for lists
Keyed lists keep their common prefix and suffix, and the longest increasing run of old positions. Swapping two of 1,000 rows is 2 insertBefore calls.
- 05
One listener per event type
Bubbling events are delegated to document. A handler is a property on its element, so a new closure each render costs one property write.
- 06
Still reason-react
Same JSX in mlx, Reason or OCaml [@JSX], and the same hooks with the same signatures. Pure-OCaml reason-react code is the compatibility target.
Server rendering
HTML in React's format, hydrated by compiled code.
ReactDOMServer.renderToString writes HTML in the format of React's own renderToString, with no markers, so a Melact app can also
render on a native OCaml server with server-reason-react. ReactDOM.Client.hydrateRoot takes that HTML over in document order, and clicks and typing made before the code arrives are delivered in order.
Two lines in dune. Try it on your app.
npm React components will not work; OCaml reason-react code is the target.