DEV Community

Sophia Brandt
Sophia Brandt

Posted on • Updated on • Originally published at rockyourcode.com

Learning ReasonReact Step by Step Part: 2

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 }))
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
johnridesabike profile image
John Jackson

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:

module UseForm = {
  [@react.component]
  let make = (~callback) => {
      ...
    }
  }
};

You should just use this:

let useForm = (~callBack) => {
  ...
}

Inside your handleChange function, targetName and targetValue are both functions, not strings, which is probably where your compiler error is coming from.

Try this instead:

let targetName = evt->ReactEvent.Form.target##name;

(Similarly, {j|$targetName|j} compiles to JS String(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 or Belt.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.

Collapse
 
sophiabrandt profile image
Sophia Brandt

Thank you, that's super helpful.
I'm very grateful for your feedback. It will tremendously help in continuing the project!

Collapse
 
yawaramin profile image
Yawar Amin

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...