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,
};
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
};
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}
/>
// ...
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)
};
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);
//...
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"/>;
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>;
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>)
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.