Hi, I'm Vaclav Zeman and I build apps that users love.
Currently working on web stuff @SecurityScorecard, previously @Avast.

Handling forms in ReasonReact

August 10, 2019 | 3 min read

For the past weeks I've been learning ReasonML and I struggled to find good resources that are using new version of the language. This is my small attempt at building a overview of how I implemented form handling in my ReasonReact app.

Let's start by creating a simple Input component:

Creating Input component

let getValue = e => ReactEvent.Form.target(e)##value;

let make = (~onChange, ~name, ~value) =>
    onChange={e => getValue(e) |> onChange(name)}
  • accessing e.target.value is a bit cumbersome, so getValue fn is created for simplicity
  • in onChange handler we're then basically doing just onChange(e.target.value).

Creating Form component

let make = () => {
  let (state, dispatch) =
    React.useReducer(Form.reducer, Form.initialState);

  let handleChange = (name, value) =>
    Form.InputChange(name, value) |> dispatch;

  let handleSubmit = e => {

  <form onSubmit=handleSubmit>
      {"Current Balance" |> ReasonReact.string}
      {"Annual Income" |> ReasonReact.string}
      {"Calculate" |> ReasonReact.string}
  • leveraging useReducer hook to create local state as it's great fit for ReasonML's pattern matching (more on that later).
  • handleChange dispatches a generic InputChange variant that has following parameters - (name: variant, value: string)
  • handleSubmit does e.preventDefault() and dispatches Submit variant to the reducer
  • render part then contains 2 controlled Inputs

Updating the Form

In the Form component, we're using Form.reducer and Form.initialState from Form module when passing them to the useReducer hook. I like this approach more than having them in the component straightaway as it imo looks a bit cleaner.

The Form module looks like this:

module Form = {
  type fields =
    | CurrentBalance
    | Income;

  type formAction =
    | InputChange(fields, string)
    | Submit;

  type state = {
    currBalance: string,
    income: string,
    hasSubmitted: bool,

  let initialState = {
    currBalance: "0",
    income: "0",
    hasSubmitted: false,

  let updateFormState = (state: state, field: fields, value: string) =>
    switch (field) {
    | CurrentBalance => {...state, currBalance: value}
    | Income => {...state, income: value}

  let reducer = (state, action) =>
    switch (action) {
    | InputChange(field, value) => updateFormState(state, field, value)
    | Submit => {...state, hasSubmitted: true}

At the top we're declaring the field names and formAction as variants. We're then creating initialState record (with its type declared just above) that is passed to useReducer in our Form component.

Let's take a look at the reducer. We're using pattern matching to update state based on formActions that are dispatched. When InputChange is dispatched, we then call our helper function updateFormState that creates a new state record with appropriate property (also using pattern matching).

Note that the reducer probably could be some generic function not related to specific form (unlike updateFormState).

If we e.g. wanted to send data to API upon submit, we could update our Form component handleSubmit function like this:

let handleSubmit = e => {

  // At this point state is updated =>