Mastodon hachyterm.io

UPDATE:
ReasonML + BuckleScript is now Rescript.
As the ecosystem has changed around those tools, this blog post is not accurate anymore.


In my last post I tried to create a custom hook function for React forms.

That didn’t work as I expected. Some kind folks helped me out and gave me some suggestions.

Let’s pivot and try something different. Instead of creating a custom hook, I’ll take a step back and add the logic to the Form component. Maybe I can decouple it later.

Using a Js.Dict to store data (email and password) proved to be difficult and seems to be an anti-pattern.

The code we have so far is pretty bare-bones and can be seen on GitHub.

useReducer Hook With ReasonReact

As an alternative, I will write a useReduce hook and add the state as a ReasonML Record.
The good news is that records are typed. The bad news is that field names (keys) are fixed. So, I’ll have to hard-code the data that I want to store.

/* src/Form.re */
type state = {
  email: string,
  password: string,
};

We set up our “storage container” type where email and password are strings.

useReducer almost works the same as in React.

Let’s write the actions:

/* src/Form.re */
type action =
  | SetEmail(string)
  | SetPassword(string)
  | SubmitForm;

When someone types into the email field, we have to store the input. The SetEmail action/function takes a parameter with the type string.
The same is true for the password.
And after that, we have to handle how to submit the form values. The SubmitForm action doesn’t take any arguments.

Now, for the useReducer:

/* src/Form.re */
//...

let reducer = (state, action) => {                   // (A)
  switch (action) {
    | SetEmail(email) => {...state, email}           // (B)
    | SetPassword(password) => {...state, password}
    | SubmitForm => {                                // (B)
      Js.log({j|Form submitted with values: $state|j});
      {email: "", password: ""};
    };
  }
};


[@react.component]
let make = () => {
  let initialState = {email: "", password: ""};    // (D)

  let (state, dispatch) = React.useReducer(reducer,initialState); // (E)

On line A, we create the reducer function with a switch statement on each action.
Our state is a Record, so we can use the spread syntax to update it (that looks like JavaScript!) (see line B).
SetEmail and SetPassword are almost identical.
SubmitForm (line C) uses a JavaScript console.log to log out our state. Then it resets the state to empty strings.
We have to use the strange looking syntax for string interpolation.

Inside the Form component I create an initial state with an empty email and password string (line D).

In React, we use a de-structured array to initialize the useReducer, i.e.:

const [state, dispatch] = React.useReducer(reducerFunction, initialState)

Reason uses a tuple, but other than that, it looks similar to React (line E).

Now, we only have to hook up the dispatch function to our JSX:

/* src/Form.re */
//
  let valueFromEvent = evt: string => evt->ReactEvent.Form.target##value; // (A)

  <div className="section is-fullheight">
    <div className="container">
      <div className="column is-4 is-offset-4">
        <div className="box">
          <form
            onSubmit={
              evt => {
                ReactEvent.Form.preventDefault(evt);
                dispatch(SubmitForm);
              }
            }>
            <div className="field">
              <label className="label"> {"Email Address" |> str} </label>
              <div className="control">
                <input
                  className="input"
                  type_="email"
                  name="email"
                  value={state.email}
                  required=true
                  onChange={evt => valueFromEvent(evt)->SetEmail |> dispatch} // (B)
                />
              </div>
            </div>
            <div className="field">
              <label className="label"> {"Password" |> str} </label>
              <div className="control">
                <input
                  className="input"
                  type_="password"
                  name="password"
                  value={state.password}
                  required=true
                  onChange={
                    evt => valueFromEvent(evt)->SetPassword |> dispatch // (B)
                  }
                />
              </div>
            </div>
            <button
              type_="submit" className="button is-block is-info is-fullwidth">
              {"Login" |> str}
            </button>
          </form>
        </div>
      </div>
    </div>
  </div>;
};

What’s going on here?

I stole line A from Jared Forsythe’s tutorial:

In JavaScript, we’d do evt.target.value to get the current text of the input, and this is the ReasonReact equivalent. ReasonReact’s bindings don’t yet have a well-typed way to get the value of an input element, so we use ReactEvent.Form.target to get the “target element of the event” as a “catch-all javascript object”, and get out the value with the “JavaScript accessor syntax” ##value.

This is sacrificing some type safety, and it would be best for ReasonReact to just provide a safe way to get the input text directly, but this is what we have for now. Notice that we’ve annotated the return value of valueFromEvent to be string. Without this, OCaml would make the return value ‘a (because we used the catch-all JavaScript object) meaning it could unify with anything, similar to the any type in Flow.

We’ll use this function to hook it up to our onChange function for the password and email fields (see line B).
First, we take the event and extract its value, then we pipe the function to our SetEmail or SetPassword action and lastly to our dispatch.

Why -> and |>?

The first one is Pipe First:

-> is a convenient operator that allows you to “flip” your code inside-out. a(b) becomes b->a. It’s a piece of syntax that doesn’t have any runtime cost.

The other one is Pipe Forward/Pipe Last/Reverse-Application Operator. It basically does the same. But some functions require you to add the thing that you pipe as the first argument, and some as the last.
It’s a bit ugly. Most JavaScript and BuckleScript interop requires pipe-first. Ocaml and Reason native code works mostly with pipe-last.

Code Repository

The complete code is on GitHub.

Thoughts

useReducer works well with ReasonReact and will be very familiar to a React developer.
I like ReasonML’s pattern matching and it’s a good fit for useReducer.

Further Reading