UPDATE:
ReasonML + BuckleScript is now Rescript.
As the ecosystem has changed around those tools, this blog post is not accurate anymore.
By now we have the HTML/JSX skeleton for our input form: a simple login form styled with Bulma.
(The code is available on Github.)
Stumbling Blocks With ReasonReact
The idea for this blog post series was to create a ReasonReact form with hooks to learn how ReasonML and ReasonReact work.
I took inspiration from James King’s tutorial on Using Custom React Hooks to Simplify Forms. When I read it at the beginning of the year, it helped me to understand how the new React Hooks API works.
In the article James creates a custom useForm
hook, so that’s what I wanted to create in ReasonReact, too.
When you have HTML forms, you will need to get the values of the HTML element (target), so that you can store it somewhere.
In React, you’d use the useState
hook or a class component and store the values as state.
You could store each value as a string, or store all values as a JavaScript object, for example.
The aforementioned blog post uses a JavaScript object with computed keys:
const handleChange = event => {
event.persist()
setValues(values => ({ ...values, [event.target.name]: event.target.value }))
}
ReasonML doesn’t use objects in the same way that Javascript does.
But we do need a data structure that can handle compound data with keys and values (a “hash map”). Of course, Reason offers something like that: the Record.
Records are immutable by default and typed! But they don’t support computed keys, you have to know the keys beforehand.
So the approach above doesn’t work with ReasonML out of the box.
BuckleScript to the rescue! BuckleScript does a good job of explaining what we use JavaScript objects for. And the documentation offers advice on how and what to use.
So, Records won’t work, let’s use a JS.Dict:
let myMap = Js.Dict.empty();
Js.Dict.set(myMap, "Allison", 10);
Let’s try to create the useForm
hook in ReasonReact (the following code doesn’t work):
/* inside src/Form.re */
module UseForm = {
[@react.component]
let make = (~callback) => {
let valuesMap = Js.Dict.empty();
let (values, setValues) = React.useState(() => valuesMap); // (A)
let handleChange = (evt) => {
let targetName = evt:string => evt->ReactEvent.Form.target##name; // (B)
let targetValue = evt:string => evt->ReactEvent.Form.target##value; // (B)
let payload = Js.Dict.set(valuesMap,{j|$targetName|j},targetValue); // (C)
ReactEvent.Form.persist(evt);
setValues(payload); // (D)
}
}
};
First, we set up an empty Js.Dict
as the initial value for the useState
hook (line (A)
).
Inside the handleChange
function we have to tell ReasonReact that the HTML target name and the HTML target value are strings (line (B)
).
Then we use the Js.Dict.set
function to add the new values to the dictionary (line (C)
) and finally try to set those values with the useState
function ((D)
).
I had to use BuckleScript’s string interpolation syntax to create the Js.Dict
key (line (C)
).
Unfortunately, that doesn’t work. The compiler complains on line line (D)
:
Error: This expression has type unit but an expression was expected of type Js.Dict.t(ReactEvent.Form.t => string) => Js.Dict.t(ReactEvent.Form.t => string)
You could always embed raw JavaScript into Reason to work around these issues, but it’s strongly discouraged.
As a newbie I’m not sure on how to continue at the moment.
How do you merge JS.Dict
objects? The interface looks like a JavaScript Map, but using the “object spread syntax” doesn’t work either. ReasonReact uses this syntax to update their immutable records, but it doesn’t work with BuckleScript’s Js.Dict
.
Furthermore, how can I use the useState
hook with a Js.Dict
?
Perhaps I’m using an anti-pattern here, and that’s why it is so hard to achieve the JavaScript solution in ReasonReact.
I’m also not sure about the file structure. Reason encourages fewer files and nested modules, but how does that work with custom (React) hooks?