Learning ReasonReact Step by Step Part: 7
Tags: reason, react, learning, tutorial
Published: 2019-10-06 05:21:18

---
title: Learning ReasonReact Step by Step Part: 7
published: true
description: Learning how ReasonReact works from a beginner's perspective
tags: reasonml, react, learning, tutorial
series: Learning ReasonReact Step by Step
canonical_url: https://www.rockyourcode.com/learning-reason-react-step-by-step-part-7
---

**UPDATE**:  
ReasonML + BuckleScript is now [Rescript](https://rescript-lang.org/blog/bucklescript-is-rebranding).  
As the ecosystem has changed around those tools, this blog post is not accurate anymore.

---

## What We've Done So Far

We've [laid the groundwork for adding form validation to our app][reasonstep6].  
We defined our types; we have a simple form and a functioning `useForm` custom hook in a separate module.

## Create Form Rules And State Management

Let's define our validation rules[^1].

```reasonml
/* inside UseForm.re */

let registerFormRules: FormTypes.formRules = [| // (A)
  {
    id: 0,
    field: "username",
    message: "Username must have at least 5 characters.",
    valid: false,
  },
  {
    id: 1,
    field: "email",
    message: "Email must have at least 5 characters.",
    valid: false,
  },
  {
    id: 2,
    field: "email",
    message: "Email must be a valid email address.",
    valid: false,
  },
  {
    id: 3,
    field: "password",
    message: "Password must have at least 10 characters.",
    valid: false,
  },
|];

let loginFormRules: FormTypes.formRules = [| // (A)
  {id: 0, field: "email", message: "Email is required.", valid: false},
  {
    id: 1,
    field: "email",
    message: "Email must be a valid email address.",
    valid: false,
  },
  {id: 2, field: "password", message: "Password is required.", valid: false},
|];
```

As mentioned in [my last post][reasonstep6], we use an Array to hold each rule. Reason's syntax for Arrays looks strange.

In Reason, you can set up a (linked) List with square brackets: `[]`.  
Thus, you need **a different way to create an Array: square brackets with delimiters**: `[||]`.  
You can read more about this on [ReasonML's documentation page][listandarray].

Please note that we have to tell Reason the type of the form rules (see line `A`). Reason can't infer the type, as we've defined it in a different module:

```reasonml
/* src/FormTypes.re */

type formState = {
  username: string,
  email: string,
  password: string,
};

type formRule = {
  id: int,
  field: string,
  message: string,
  valid: bool,
};

type formRules = array(formRule);
```

The form rules are one piece of state. We'll have to find a way to add validation logic, and we'll want to display the validation rules to the user.

**☞ [View Demo][demo]**

The status of a form rule depends on what the user types into the form field. We already hold that piece of state in our `useForm` custom hook inside a `useReducer` (with the type `FormTypes.formState`).

In my app, I created a separate `useReducer` for working with the form rules. Unfortunately, that means that I have to **synchronize two pieces of state** (the form data from the fields and the validation rules that depend on the form data).

A better way might be to **[derive the state][derivedstate]**, but then you have to shove everything into one storage container instead of having two state containers.

For now, I'll work with two distinct pieces of state, but maybe I can work out how the other approach works in a later blog post.

Inside the `useForm` hook, we'll create _two_ new `useReducers`. Why two?  
One will be for our register form rules, and one for the login form rules. Reason distinguishes between those two. The compiler throws errors if you try to use them interchangeably.

```diff
/* src/UseForm.re */


let useForm = (~formType, ~callback) => {
  let valueFromEvent = evt: string => evt->ReactEvent.Form.target##value;
  let nameFromEvent = evt: string => evt->ReactEvent.Form.target##name;
  let (formData, dispatchFormData) =
    React.useReducer(formReducer, initialFormData);


+  let (registerFormRules, dispatchRegisterFormRules) =
+    React.useReducer(registerFormRulesReducer, registerFormRules); // (A)
+
+  let (loginFormRules, dispatchLoginFormRules) =
+    React.useReducer(loginFormRulesReducer, loginFormRules); // (A)
+
+  let formRules = // (B)
+    switch (formType) {
+    | "register" => registerFormRules
+    | "login" => loginFormRules
+    | _ => [||]
+    };

// same code as before

+ (formData, formRules, handleChange, handleSubmit); // (C)
}
```

Differentiating between those two kinds of rules (either a set of rules for login or register) proved to be complicated.

**Reason requires you to be clear about different types.** The rules, the dispatch function, and the action creators for a registration form are different from a login form. Although the logic is (mostly) the same, Reason doesn't cut you slack. You have to set up _two_ `useReducers` with two distinct rules and two action creators and dispatch functions (`A`).

On line B, I pattern-match on the type of form and initialize another value called `formRules`, which I set to either `registerFormRules` or `loginFormRules`.

**Bindings are immutable in Reason**, but you can "overwrite" them by adding a new `let` binding (which is pattern-matching under the hood, too). Read more about this at the [docs][letbinding].

Here we just conditionally set a binding for `formRules` (similar to a variable binding in JavaScript) depending on the kind of form we receive as a parameter of the `useForm` function.

Lastly, we return the `formRules` (see line `C`), so that a component can render them. Remember that Reason has an implicit return, so it returns the last value(s).

## Reducer And Action Creators

How do these look?

```reasonml
/* src/UseForm.re */

type registerFormRulesAction =
  | UsernameLongEnough(string)
  | EmailLongEnough(string)
  | EmailForRegistrationValid(string)
  | PasswordLongEnough(string);

type loginFormRulesAction =
  | EmailRequired(string)
  | EmailForLoginValid(string)
  | PasswordRequired(string);
```

The action creators map to their form validation rules. Each action will check on each rule.

Now, the workhorse of the logic: the reducer functions.  
Again, you have to create one for each type of form.

Let's remember how our form rules look like: its' an Array of records where each record has a key of id, field, message, and valid.

```reasonml
let registerFormRules: FormTypes.formRules = [|
  {
    id: 0,
    field: "username",
    message: "Username must have at least 5 characters.",
    valid: false,
  },
  // more rules
|];

```

We want to check if the input satisfies the validation rule, and then toggle the `valid` key.

But we have to remember that **we don't want to mutate state directly**. After each action, we want to return a new Array with _all rules_. If a rule is satisfied, we will change the `valid` flag, but the other rules will have to remain untouched.

We must make sure that the [React hooks (`useState` and `useReducer`) correctly handle and update state changes. We want React to re-render immediately after a rule's `valid` key was changed][stalestate].

Plus, records are immutable.

Thus, we have to traverse the complete Array, pick the rule that we're validating, replace it with a new rule with a different `valid` key, and copy the rest of the Array.

`Array.map` works the same as in JavaScript, but the syntax looks a tad different.

Let's create two helper functions which will toggle the `valid` key:

```reasonml
/* src/UseForm.re */

let setRuleToValid = (rules: FormTypes.formRules, id) =>
  Array.map(
    rule => rule.FormTypes.id === id ? {...rule, valid: true} : rule,
    rules,
  );

let setRuleToInvalid = (rules: FormTypes.formRules, id) =>
  Array.map(
    rule => rule.FormTypes.id === id ? {...rule, valid: false} : rule,
    rules,
  );
```

The functions take a rules Array (of type `FormTypes.formRules`) and an id (of type `int` which Reason infers) as input.  
Then we'll map over that array with `Array.map`. The Array collection is the second argument.  
The first argument is the function we use on each rule in the Array:  
If the input id is the same as the id of the rule, copy it and update the `valid` key, otherwise, leave it untouched.

The function would look almost the same in (functional) JavaScript:

```js
const setRuleToValid = (rules, id) => {
  return rules.map(rule => (rules.id === id ? { ...rule, valid: true } : rule))
}
```

Here are the two reducer functions now:

```reasonml
/* src/UseForm.re */

let registerFormRulesReducer =
    (state: FormTypes.formRules, action: registerFormRulesAction) =>
  switch (action) {
  | UsernameLongEnough(username) =>
    username |> String.length >= 5 ?
      setRuleToValid(state, 0) : setRuleToInvalid(state, 0)
  | EmailLongEnough(email) =>
    email |> String.length >= 5 ?
      setRuleToValid(state, 1) : setRuleToInvalid(state, 1)
  | EmailForRegistrationValid(email) =>
    email |> validEmail ?
      setRuleToValid(state, 2) : setRuleToInvalid(state, 2)
  | PasswordLongEnough(password) =>
    password |> String.length >= 10 ?
      setRuleToValid(state, 3) : setRuleToInvalid(state, 3)
  };

let loginFormRulesReducer =
    (state: FormTypes.formRules, action: loginFormRulesAction) =>
  switch (action) {
  | EmailRequired(email) =>
    email |> String.length > 0 ?
      setRuleToValid(state, 0) : setRuleToInvalid(state, 0)
  | EmailForLoginValid(email) =>
    email |> validateEmail ?
      setRuleToValid(state, 1) : setRuleToInvalid(state, 1)
  | PasswordRequired(password) =>
    password |> String.length > 0 ?
      setRuleToValid(state, 2) : setRuleToInvalid(state, 2)
  };
```

Some code duplication, but I couldn't find a better way to write this.

Each pattern-match pipes the field input into a function that checks for validity.

Here's the helper function for a valid email that uses regular expressions[^1]:

```reasonml
/* src/Form.re */

let validEmail = email => {
  let re = [%bs.re
    "/^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/"
  ];
  email |> Js.Re.test_(re);
};
```

I used [Js.Re][bsjsre] from the BuckleScript toolchain to test if the input matches the regular expression.

Finally, let's wire everything together in the `useForm` function:

```reasonml
let useForm = (~formType, ~callback) => {
// previous code


let validate = (~formData=formData, ()) =>
    switch (formType) {
    | "register" =>
      formData.username->UsernameLongEnough |> dispatchRegisterFormRules;
      formData.email->EmailLongEnough |> dispatchRegisterFormRules;
      formData.email->EmailForRegistrationValid |> dispatchRegisterFormRules;
      formData.password->PasswordLongEnough |> dispatchRegisterFormRules;
    | "login" =>
      formData.email->EmailRequired |> dispatchLoginFormRules;
      formData.email->EmailForLoginValid |> dispatchLoginFormRules;
      formData.password->PasswordRequired |> dispatchLoginFormRules;
    | _ => ()
    };

// more code
};

```

The `validate` function takes `formData` (our form state: username, email, password) as the first argument.  
We label that argument with the tilde `~`. (Read more about labeled arguments on the [Reason documentation][labeled]).  
All functions are automatically [curried](https://en.wikipedia.org/wiki/Currying). We now have to pass the unit type (`()`) as a second argument. Every function takes at least one argument, and with labeled arguments, we have to pass unit as the second argument.

See how we make a distinction between "login" and "register"? We had to create two `useReducer` hooks with separate dispatch functions.

We pattern-match on each input field and dispatch it to the applicable reducer function.

Phew, that was a lot.

### Frustrations

I couldn't find a way to decouple login and registration forms.

Now, the app holds the state for the form data, plus both the validation rules for login and register.  
Perhaps I would have to extract this logic in _yet another_ custom hook?

Plus, there's some code duplication that I would instead want to generalize. But I'm not sure how to tackle this problem right now.

[authenticationcourse]: https://testdriven.io/courses/auth-flask-react/
[reasonstep6]: https://www.rockyourcode.com/learning-reason-react-step-by-step-part-6
[listandarray]: https://reasonml.github.io/docs/en/list-and-array#array
[demo]: https://reason-form.firebaseapp.com
[derivedstate]: https://kentcdodds.com/blog/dont-sync-state-derive-it
[stalestate]: https://johno.com/react-hooks-stale-state
[bsjsre]: https://bucklescript.github.io/docs/en/regular-expression#docsNav
[letbinding]: https://reasonml.github.io/docs/en/let-binding#bindings-are-immutable
[labeled]: https://reasonml.github.io/docs/en/function#labeled-arguments

[^1]: The following code is inspired by the course [Microservices with Docker, Flask, and React](https://testdriven.io/courses/microservices-with-docker-flask-and-react/part-one-docker-config/). (I shamelessly converted the regex function for email validation 1-to-1 from JavaScript to Reason.) The course is not available anymore, as the author offers a [new course on Authentication with Flask, React, and Docker][authenticationcourse].