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)
becomesb->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
.
Top comments (2)
I'm beginning to lose you. I just don't understand why someone will use Reason when something simple such as merging two objects or getting the value of the event target gets "Un-Reasonable" complicated in the end?
Good question.
Updating the state in a reducer works the same in React though. If you mutate an object you hold in
useState
oruseReducer
with Javascript, you'll get a bug, too.JavaScript allowing for dynamic values in objects is nice and easy for a developer but it's not type-safe and AFAIK also bad for the compiler as it cannot optimize before execution, as the object is like a "black box".
I'm not sure how Typescript handles that, but if you define an interface for the object, you are bound to those limitations, too.
But ReasonML has a lot of warts, one of them Javascript interoperability.
This blog post series is not my goal to sell you ReasonML or ReasonReact, but my exploration of creating an app.
I actually like the type safety, but I'm not sure if you couldn't have the same experience with Typescript.