DEV Community

Sophia Brandt
Sophia Brandt

Posted on • Originally published at rockyourcode.com on

Learning ReasonReact Step by Step Part: 4

UPDATE:

ReasonML + BuckleScript is now Rescript.

As the ecosystem has changed around those tools, this blog post is not accurate anymore.


So far, we've created a simple form component with a useReducer hook.

This form works with an email and a password - it could be a login form.

But what if we would like to use the same logic to create a register form, too?

Make The Form Component Re-Usable With a Custom Hook

We know the shape of our data: we have three form fields: email, password, and username. We will only show the username field on the register page.

But we have to set up a record for all of our state:

/* src/Form.re */
type state = {
  username: string, // *new
  email: string,
  password: string,
};
Enter fullscreen mode Exit fullscreen mode

Let's extract our useReducer hook into a separate function and adjust the actions. First, the initial state of our form, the action type and the reducer function:

/* src/Form.re */
let initialState = {username: "", email: "", password: ""};

type action =
  | SetUsername(string)
  | SetEmail(string)
  | SetPassword(string) // *new
  | ResetState;         // *new

let reducer = (state, action) =>
  switch (action) {
  | SetUsername(username) => {...state, username}
  | SetEmail(email) => {...state, email}
  | SetPassword(password) => {...state, password} // *new
  | ResetState => initialState                   // *new
  };
Enter fullscreen mode Exit fullscreen mode

In our last attempt we used useReducer inside the component, and also hooked up the dispatch functions inside the component's JSX.

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

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

  // ...

    <input
        className="input"
        type_="email"
        name="email"
        value={state.email}
        required=true
        onChange={evt => valueFromEvent(evt)->SetEmail |> dispatch}
      />

 // ...

Enter fullscreen mode Exit fullscreen mode

Instead, I want to create a custom hook that deals with the form actions and with handling state.

let useForm = (~callback) => { // (A)
  let valueFromEvent = evt: string => evt->ReactEvent.Form.target##value;
  let nameFromEvent = evt: string => evt->ReactEvent.Form.target##name;

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

  let handleChange = evt => {
    ReactEvent.Form.persist(evt);
    switch (nameFromEvent(evt)) {
    | "username" => valueFromEvent(evt)->SetUsername |> dispatch
    | "email" => valueFromEvent(evt)->SetEmail |> dispatch
    | "password" => valueFromEvent(evt)->SetPassword |> dispatch
    | _ => ()   // (B)
    };
  };

  let handleSubmit = evt => {
    ReactEvent.Form.preventDefault(evt);
    callback();            // (A)
    dispatch(ResetState);  // (C)
  };

  (state, handleChange, handleSubmit); // (D)
};
Enter fullscreen mode Exit fullscreen mode

The custom hook takes a callback function (A) that we'll use when we submit the form. Now different forms could add different logic!

The handleChange function mirrors what we had before. We use pattern-matching on each action. All actions deal with the state of the form: they update it or reset it.

What's all this nameFromEvent and valueFromEvent stuff?

We have to somehow interact with the DOM - in JavaScript, it would be evt.target.value and evt.target.name.

For example, if the target name is "password," then update the password state with the value we got out of the HTML form.

But wait! The action variant also has the option to reset a form. We don't want to handle this case in handleChange. Instead, we dispatch it (see on line C: ResetState) when we submit the form.

Our pattern-matching in handleChange isn't exhaustive. We don't handle all possible cases.

That's why we have to set up a "catch-all" case in line A. The underscore matches on everything. We don't want to return anything, so we return the Unit type (a type that represents "no value") - a.k.a. empty brackets (see line B).

In the end, we have to return state, handleChange, and handleSubmit (D), so that we can use it in our form component as a custom hook.

Use The Custom Hook In The Form Component

Now, let's take advantage of our custom hook inside the React component:

/* src/Form.re */
[@react.component]
let make = (~formType) => {
  let logger = () => Js.log("Form submitted");

  let (state, handleChange, handleSubmit) = useForm(~callback=logger);

  //...

Enter fullscreen mode Exit fullscreen mode

The logger function is our callback for useForm. Then we de-structure state, handleChange, and handleSubmit from useForm.

Our component will take a prop called formType. The formType will tell us if it's the Register page or the Login Page.

For example, in src/App.re it would look like this:

[@react.component]
let make = () => <Form formType="login"/>;
Enter fullscreen mode Exit fullscreen mode

Now, we'll have to add the logic to the JSX:

// ...

<div className="section is-fullheight">
    <div className="container">
      <div className="column is-4 is-offset-4">
        <h1 className="is-size-1 has-text-centered is-capitalized">
          {formType |> str}   // (A)
        </h1>
        <br />
        <div className="box">
          <form onSubmit=handleSubmit>      // (B)
            {
              formType === "register" ?     // (C)
                <div className="field">
                  <label className="label"> {"Username" |> str} </label>
                  <div className="control">
                    <input
                      className="input"
                      type_="text"
                      name="username"
                      value={state.username}
                      required=true
                      onChange=handleChange  // (D)
                    />
                  </div>
                </div> :
                ReasonReact.null
            }
            <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=handleChange   // (D)
                />
              </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=handleChange // (D)
                />
              </div>
            </div>
            <button
              type_="submit"
              className="button is-block is-info is-fullwidth is-uppercase">
              {formType |> str} // (A)
              <br />
            </button>
          </form>
        </div>
      </div>
    </div>
  </div>;
Enter fullscreen mode Exit fullscreen mode

On lines A we can see that the form will display a heading or a button text depending on the formType props.

Line B shows how we submit a form with the custom useForm function handleSubmit.

Line C shows how we conditionally display the username fields, if our form is the register form (formType is the props we get from the main App.re).

When we don't want to render the fields, we have to pass ReasonReact.null.

In JavaScript, you can do a boolean render like so:

(formType === "register" && (<JSX here>)
Enter fullscreen mode Exit fullscreen mode

That's discouraged in ReasonML. You have to be explicit about what happens if you don't meet the condition.

Lines D show that we have to pass down the handleChange function to each onChange input field as well. Our useForm custom hook encapsulates the logic on how to handle state inside the useForm hook. That makes it easier to understand our code.

Code Repository

The complete Form module is available on GitHub.

Thoughts

After some initial hiccups, it's surprisingly straightforward to write ReasonReact.

ReasonReact keeps close to React.js.

You can "think in React.js" and port it over to ReasonReact/ReasonML. The new JSX syntax (released earlier this year) also almost feels like native React.js.

Sometimes the similarities are almost a detriment, as they hide that Reason and JavaScript are different languages after all.

Pattern-matching is one of the killer features of Reason. I came to enjoy it when learning Elixir, and I am happy to use it on the front-end with ReasonReact now, too.

Further Reading

Top comments (0)