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?
Top comments (3)
I'm enjoying your "Learning ReasonReact" series so far. I good luck with your Reason project!
Here's a few things that may help:
A React hook is just a function, not a React component. Instead of this:
You should just use this:
Inside your
handleChange
function,targetName
andtargetValue
are both functions, not strings, which is probably where your compiler error is coming from.Try this instead:
(Similarly,
{j|$targetName|j}
compiles to JSString(targetName)
, so it's just the JS string representation of the function.)If you're coming from a JavaScript background and Reason is giving you trouble, it's always good to look at the
*.bs.js
files and see how they're actually being compiled. Often, that will help explain what's really going on in the Reason code.To answer your other questions:
the Js.Dict type in Reason is useful for interoperability with JavaScript, but Reason doesn't provide a lot of tools for working with it (like merging dicts). If you're writing pure Reason code, then you may want to look into the BuckleScript data types. Unfortunately, their documentation isn't very newbie-friendly, but they're great to use once you get the hang of them. To replace a dict, you'll want a
Belt.Map.String
orBelt.HashMap.String
. They work almost exactly the same way that the standard OCaml data containers work: ocaml.org/learn/tutorials/comparis...With forms, most of the time you can control exactly what the fields are, so a record is probably more appropriate than a dict or a map. ReasonReact (and ReactJS) discourage using
useState
with complex data types (dicts, records, maps, etc.).useReducer
is a much better option. It works in ReasonReact almost exactly the same way it does in ReactJS.As for file (module) structure, you can put as many hooks (or components!) as you want inside each file. Hooks are just functions. Components are modules, and you can nest modules inside modules.
I hope that info helps point you in the right direction. For anyone reading this who's having trouble using ReasonReact for the first time, don't hesitate to check out the community! The Reason forums and Discord are active and friendly to newbies.
Thank you, that's super helpful.
I'm very grateful for your feedback. It will tremendously help in continuing the project!
Hi Sophia, you are on the right track. I agree with John that
Belt.Map.String
seems like the right data structure here. It's an immutable data structure that offers get/set/update/merge operations, pretty much exactly what you need.Regarding file structure, I wrote something about that, which you may find helpful: dev.to/yawaramin/a-modular-ocaml-p...