NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

November 28, 2025
•
NordVarg Team
•

ReasonML and Melange: Type-Safe React Development with OCaml

Programming Languagesreasonmlmelangereactocamlfunctional-programmingfrontend
6 min read
Share:

ReasonML promised to bring OCaml's powerful type system to JavaScript development. Now, with Melange, that promise is more real than ever. Melange is a modern OCaml-to-JavaScript compiler that enables you to write React applications with the full power of OCaml's type system, pattern matching, and functional programming paradigms.

Why ReasonML + Melange?#

The Problem with TypeScript#

TypeScript is great, but it has fundamental limitations:

  • Unsound type system: any, type assertions, and structural typing allow runtime errors to slip through.
  • Null/undefined chaos: TypeScript has both null and undefined, and they're not properly handled by the type system.
  • No exhaustiveness checking: Pattern matching is limited, and the compiler won't warn you about unhandled cases.

The OCaml Advantage#

OCaml (and by extension, ReasonML) offers:

  • Sound type system: If it compiles, it won't have type errors at runtime.
  • Algebraic Data Types (ADTs): Model your domain precisely with variants and records.
  • Exhaustive pattern matching: The compiler forces you to handle every case.
  • No null: OCaml uses option types, making null handling explicit and safe.

What is Melange?#

Melange is a backend for the OCaml compiler that targets JavaScript. Unlike the older BuckleScript/ReScript, Melange:

  • Stays close to OCaml syntax and semantics
  • Integrates with the OCaml ecosystem (opam, dune)
  • Generates readable, performant JavaScript
  • Has excellent React bindings via reason-react

Setting Up a Melange + React Project#

Installation#

bash
1# Install opam (OCaml package manager)
2bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
3
4# Initialize opam
5opam init -y
6eval $(opam env)
7
8# Create a new switch for your project
9opam switch create . 5.1.1 --deps-only -y
10eval $(opam env)
11
12# Install Melange and dependencies
13opam install melange reason-react reason-react-ppx dune
14

Project Structure#

plaintext
1my-react-app/
2├── dune-project
3├── dune
4├── package.json
5├── src/
6│   ├── dune
7│   ├── App.re
8│   └── Index.re
9└── public/
10    └── index.html
11

dune-project#

lisp
1(lang dune 3.8)
2(using melange 0.1)
3

dune (root)#

lisp
1(melange.emit
2 (target output)
3 (libraries reason-react)
4 (preprocess (pps reason-react-ppx))
5 (module_systems es6))
6

Building React Components in ReasonML#

A Simple Counter Component#

reason
1/* Counter.re */
2[@react.component]
3let make = () => {
4  let (count, setCount) = React.useState(() => 0);
5
6  <div>
7    <h1> {React.string("Counter: " ++ string_of_int(count))} </h1>
8    <button onClick={_ => setCount(c => c + 1)}>
9      {React.string("Increment")}
10    </button>
11    <button onClick={_ => setCount(c => c - 1)}>
12      {React.string("Decrement")}
13    </button>
14  </div>;
15};
16

Using Variants for State Management#

One of ReasonML's killer features is variants (sum types). Here's a more sophisticated example:

reason
1/* TodoApp.re */
2type todo = {
3  id: int,
4  text: string,
5  completed: bool,
6};
7
8type filter =
9  | All
10  | Active
11  | Completed;
12
13type state = {
14  todos: list(todo),
15  filter,
16  nextId: int,
17};
18
19type action =
20  | AddTodo(string)
21  | ToggleTodo(int)
22  | DeleteTodo(int)
23  | SetFilter(filter);
24
25let reducer = (state, action) =>
26  switch (action) {
27  | AddTodo(text) =>
28    let newTodo = {id: state.nextId, text, completed: false};
29    {
30      ...state,
31      todos: [newTodo, ...state.todos],
32      nextId: state.nextId + 1,
33    };
34  | ToggleTodo(id) =>
35    let todos =
36      state.todos
37      |> List.map(todo =>
38           todo.id == id ? {...todo, completed: !todo.completed} : todo
39         );
40    {...state, todos};
41  | DeleteTodo(id) =>
42    let todos = state.todos |> List.filter(todo => todo.id != id);
43    {...state, todos};
44  | SetFilter(filter) => {...state, filter}
45  };
46
47[@react.component]
48let make = () => {
49  let (state, dispatch) =
50    React.useReducer(
51      reducer,
52      {todos: [], filter: All, nextId: 0},
53    );
54
55  let filteredTodos =
56    switch (state.filter) {
57    | All => state.todos
58    | Active => state.todos |> List.filter(t => !t.completed)
59    | Completed => state.todos |> List.filter(t => t.completed)
60    };
61
62  <div>
63    <h1> {React.string("Todo App")} </h1>
64    <input
65      onKeyDown={e =>
66        if (ReactEvent.Keyboard.key(e) == "Enter") {
67          let value = ReactEvent.Form.target(e)##value;
68          dispatch(AddTodo(value));
69          ReactEvent.Form.target(e)##value #= "";
70        }
71      }
72      placeholder="What needs to be done?"
73    />
74    <div>
75      <button onClick={_ => dispatch(SetFilter(All))}>
76        {React.string("All")}
77      </button>
78      <button onClick={_ => dispatch(SetFilter(Active))}>
79        {React.string("Active")}
80      </button>
81      <button onClick={_ => dispatch(SetFilter(Completed))}>
82        {React.string("Completed")}
83      </button>
84    </div>
85    <ul>
86      {filteredTodos
87       |> List.map(todo =>
88            <li key={string_of_int(todo.id)}>
89              <input
90                type_="checkbox"
91                checked={todo.completed}
92                onChange={_ => dispatch(ToggleTodo(todo.id))}
93              />
94              <span> {React.string(todo.text)} </span>
95              <button onClick={_ => dispatch(DeleteTodo(todo.id))}>
96                {React.string("Delete")}
97              </button>
98            </li>
99          )
100       |> Array.of_list
101       |> React.array}
102    </ul>
103  </div>;
104};
105

Advanced Patterns#

Option Types for Nullable Data#

reason
1type user = {
2  name: string,
3  email: option(string),
4};
5
6[@react.component]
7let make = (~user: user) => {
8  <div>
9    <h2> {React.string(user.name)} </h2>
10    {switch (user.email) {
11     | Some(email) => <p> {React.string("Email: " ++ email)} </p>
12     | None => <p> {React.string("No email provided")} </p>
13     }}
14  </div>;
15};
16

Result Types for Error Handling#

reason
1type apiError =
2  | NetworkError
3  | ParseError(string)
4  | NotFound;
5
6type apiResult('a) = result('a, apiError);
7
8[@react.component]
9let make = () => {
10  let (data, setData) = React.useState(() => None);
11  let (error, setError) = React.useState(() => None);
12
13  React.useEffect0(() => {
14    fetchUser()
15    |> Promise.then_(result =>
16         switch (result) {
17         | Ok(user) =>
18           setData(_ => Some(user));
19           Promise.resolve();
20         | Error(err) =>
21           setError(_ => Some(err));
22           Promise.resolve();
23         }
24       )
25    |> ignore;
26    None;
27  });
28
29  <div>
30    {switch (error) {
31     | Some(NetworkError) => <p> {React.string("Network error")} </p>
32     | Some(ParseError(msg)) =>
33       <p> {React.string("Parse error: " ++ msg)} </p>
34     | Some(NotFound) => <p> {React.string("User not found")} </p>
35     | None => React.null
36     }}
37    {switch (data) {
38     | Some(user) => <UserProfile user />
39     | None => <p> {React.string("Loading...")} </p>
40     }}
41  </div>;
42};
43

Interop with JavaScript#

Melange makes it easy to use existing JavaScript libraries:

reason
1/* Bindings for a JS library */
2[@mel.module "lodash"] external debounce: ('a => unit, int) => ('a => unit) = "debounce";
3
4[@react.component]
5let make = () => {
6  let (searchTerm, setSearchTerm) = React.useState(() => "");
7
8  let debouncedSearch =
9    React.useMemo1(
10      () => debounce(term => Js.log("Searching for: " ++ term), 300),
11      [||],
12    );
13
14  <input
15    value=searchTerm
16    onChange={e => {
17      let value = ReactEvent.Form.target(e)##value;
18      setSearchTerm(_ => value);
19      debouncedSearch(value);
20    }}
21  />;
22};
23

Build and Run#

bash
1# Build with Melange
2dune build
3
4# The output will be in _build/default/output/src/
5# You can serve it with any static server
6npx serve _build/default/output
7

Comparison: TypeScript vs ReasonML#

FeatureTypeScriptReasonML
Type SafetyUnsound (gradual typing)Sound (no runtime type errors)
Null Handlingnull and undefinedoption type
Pattern MatchingLimited (switch)Exhaustive, compiler-enforced
ImmutabilityOpt-inDefault
Learning CurveLow (JS-like)Medium (ML syntax)
EcosystemMassiveGrowing

When to Use ReasonML + Melange#

Use it when:

  • You're building complex, mission-critical UIs (trading dashboards, financial apps)
  • You want compile-time guarantees about correctness
  • Your team values functional programming
  • You're already using OCaml on the backend

Avoid it when:

  • You need a massive ecosystem of libraries
  • Your team is unfamiliar with functional programming
  • You need rapid prototyping with minimal setup

Conclusion#

ReasonML with Melange brings the rigor of OCaml to frontend development. While the ecosystem is smaller than TypeScript's, the type safety and expressiveness make it a compelling choice for teams building complex, reliable applications. If you've ever had a production bug caused by undefined is not a function, ReasonML might be worth the learning curve.

Further Reading#

  • Melange Documentation
  • ReasonML Documentation
  • Reason React
  • OCaml Manual
NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

reasonmlmelangereactocamlfunctional-programming

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 11, 2025•10 min read
Zig for Fintech: Performance, Safety, and C Interop
Programming Languageszigsystems-programming
Nov 11, 2025•10 min read
Building a Trading DSL: From Grammar to Execution
Programming LanguagesDSLtrading
Jan 10, 2025•18 min read
Dependent Types in OCaml: Type-Level Programming with GADTs
Languagesocamldependent-types

Interested in working together?