Migrating from reason-react
Change two lines. Keep everything else.
React.useState, useEffect, useContext, ReactDOM.Client,
React.Event, ReasonReactRouter …) and replaces what is underneath it. Pure-OCaml reason-react code is the compatibility target.
1
Switch two lines in dune
- (libraries reason-react)
+ (libraries melact)
- (preprocess (pps reason-react-ppx))
+ (preprocess (pps melact-ppx))Your components stay as they are: the same JSX in mlx, Reason or OCaml [@JSX], and the same hooks with the same signatures.
2
What works
Runtime
- View
- Template
- Reconcile
- delegated events
Hooks
- useState
- useReducer
- useEffect*
- useLayoutEffect*
- useMemo*
- useCallback*
- useRef
- useImperativeHandle*
- useId
React
- components
- context
- React.memo
- React.act
- React.Event (every family)
- React.createElement with makeProps
ReactDOM
- ReactDOM.Client
- ReactDOM.Style
- ReactDOM.Ref
- ReactDOMServer.renderToString
- ReactDOM.Client.hydrateRoot
Routing
- ReasonReactRouter
JSX
- Reason
- OCaml [@JSX]
- mlx
Every hook with a key is supported. Every React.Event family reads the native event; handlers see the currentTarget they were set on, as in React. Delegated mouse events skip the handlers of a disabled button, input, select or textarea and go on bubbling, as React does.
3
What changes underneath
melact-ppx writes makeProps next to every [@react.component], so React.createElement Foo.make (Foo.makeProps ~label ()) is make ~label () with a key, checked by the type checker, and no props object exists at run time.
4
Rules that matter
- Render purely. Melact relies on the Rules of React: rendering is a pure function of props, state and context. React Compiler relies on the same contract.
- Do not mutate in place. Change detection is physical equality. Code that mutates an array or a record in place and passes the same reference back will not re-render that part.
- Hooks at the top level. A hook inside an
if, amatch, a loop or a callback raises thehookalert at build time. It is a warning, not an error, so reason-react code that bends the rule still compiles. - A key resets. A child with a
keyis mounted again when the key changes, as in React, which is how React code resets a component.
5
Server rendering
ReactDOMServer.renderToString writes HTML in React's format, so React's own and server-reason-react's HTML hydrate too, and a Melact app renders natively with server-reason-react. Hydration code is compiled under-hydratable. useId stays a client counter: Melact's hydration writes every attribute from the client render, so ids inid, htmlFor and aria-* agree on the page whatever format the server used.
6
How it is checked
37/37
steps of reason-react's own demo with the same DOM under React 18 and Melact, compared after every click.
605/605
steps of five random walks through reason-seoul's TodoMVC: clicks, typing, submits and route changes.
compat/run.sh clones reason-react, melange-opam-template and reason-seoul's TodoMVC, changes two lines in their dune files and builds them against Melact. Then it renders reason-react's demo with React and with Melact and compares the DOM after every click, and walks the TodoMVC randomly comparing the DOM after every step.
The TodoMVC was written for reason-react 0.9, so both copies first get the same upgrade to reason-react 0.17 (ReactEvent. becomesReact.Event., and Index.re uses ReactDOM.Client). One demo component binds React's npm useReducer directly, which is a non-goal, so the script removes it.
7
Not yet, and limits
- Not yet:
React.createElementVariadic, code typed againstReact.component('props), Suspense on the server. - Limits: a component or custom hook that is polymorphic in its state type gets a weak type for its hook key; annotate it, or keep the state type concrete.
8
Not goals
- npm React components. react-query and other packages from npm will not work. Only OCaml reason-react code is the target.
- Suspense, concurrent rendering, useTransition semantics. useTransition and startTransition run synchronously.
- Server components.
- Signals. There is no reactive graph. A component function runs again, and its holes are compared.