Codementor Events

Introduction to ReasonReact Components

Published Oct 19, 2019

React entities are called components. They can be rendered in the DOM and values
can be passed to them via props. We will learn how to build a simple stateful
React component. We will write the beginnings of a login system focusing on the
front end side. In a later tutorial we will look at how to communicate with the
back end server.

Init project

We start by initiating a Reason project with ReasonReact and removing the
example files.

bsb -init login-app -theme react-hooks
cd login-app/src
rm *.re

Create Index.re

We need a top level React component that is attached to the DOM. In
login-app/src run touch Index.re. Edit it to look like the following.

ReactDOMRe.renderToElementWithId(<div>{ReasonReact.string("Hello World!")}</div>, "root");

We saw this in the previous tutorial. renderToElementWithId takes two arguments, JSX and
an HTML id to attach the JSX to it.

Update html file

Update the index.html to have an id of value root.
The script src is Index.js. This will be created by our webpack file after
the Reason code is compiled. It expectes to find a a file called
./src/Index.bs.js which is the compiled output of Index.re.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Example</title>
</head>
<body>
  <div id="root"></div>
  <script src="Index.js"></script>
</body>
</html>

Now we have a functional ReasonReact application. Let's look at in the browser.

yarn build
PORT=12345 yarn server

Go to 127.0.0.1:12345 in your browser and you will see "Hello World!".

Create Login.re

This is our main React component. We will start the component without any state,
just JSX. It looks pretty similar to HTML. Only two small things to note:
instead of the type prop, it is type_ and string literals and Reason string
types must be passed via (ReasonReact.string()).

[@react.component]
let make = () => {
  <div>
    <form>
      <fieldset>
        <legend> (ReasonReact.string("Login Details")) </legend>
        <div>
          <label> (ReasonReact.string("Username:")) </label>
          <input
            name="username"
            type_="string"
            placeholder="username"
          />
        </div>
        <div>
          <label> (ReasonReact.string("Password:")) </label>
          <input
            name="password"
            type_="password"
            placeholder="password"
          />
        </div>
      </fieldset>
    </form>
  </div>;
};

Update Index.re to load our login form.

ReactDOMRe.renderToElementWithId(<Login />, "root");

Login state

We first need to define all the elements that will appear in the login
components state. We use a record type with three fields.

type state = {
  username: string,
  password: string,
  show: bool,
};

username and password are the credentials for logging in. show will only
be used to prove to ourselves that the state works and render it to the screen.
Otherwise in a real login system we should not be showing a password.

Then we need a default state for when the login component is initially rendered.

let defaultState = {username: "", password: "", show: false};

Login action

This component also needs a set of actions that can occur during its lifetime.

type action =
  | UpdateUsername(string)
  | UpdatePassword(string)
  | Submit
  | Clear;

This is a sum type, a more sopisticated enum type that allows each possible value
to also hold data if needed. UpdateUsername takes a string and
updates the username in state. Same for UpdatePassword and password.
Submit and Clear will handle click events.

Login reducer

A Reason reducer is a function that can update a component's state based on a
particular action. We can also think of it as a localized event system. Here
is the reducer for our login system.

let (state, dispatch) =
  React.useReducer(
    (state, action) =>
      switch (action) {
      | UpdateUsername(username) => {...state, username}
      | UpdatePassword(password) => {...state, password}
      | Submit =>
        Js.log(state);
        {...state, show: true};
      | Clear => defaultState
      },
    defaultState,
  );

The first line is what React.useReducer returns: two values in a tuple,
the current state of type state and dispatch which is a function that
takes an action and updates the component's state. Every time the dispatch
is called and updates the component's state, we will see changes in the
rendered component.

React.useReducer takes two parameters, a function that takes the current
state and an action and returns a new state, and the default state
for rendering the component for the first time.

In the function body there is a control flow statement switch. It takes a sum
type like action and asks us to define behavior for every constructor in
action. If we do not define one, the compiler will warn us.

Let's look at each action:

| UpdateUsername(username) => {...state, username}: update the username with
a new string.

| UpdatePassword(password) => {...state, password}: update the password with
a new string.

| Submit => Js.log(state); {...state, show: true};: print the state to the
console and set show to true.

| Clear => defaultState: set state to the defaultState.

Spread operator

If you have used ES6, you maybe familiar with this syntax:
{...state, username: "jacques"}. we pass a record value prefixed with ...
and field and new value for anything we want to update. Any fields not named will stay the
same.

If the field and the new value have the same name, you can update
it with a shorthand style: {...state, username}. If the field and the variable name
you have to use both of them like this {...state, username: newUsername}.

Update Login JSX

The last step is to update the JSX we from above with state values and
dispatch some actions. We will discuss new pieces below.

  <div>
    <form>
      <fieldset>
        <legend> (ReasonReact.string("Login Details")) </legend>
        <div>
          <label> (ReasonReact.string("Username:")) </label>
          <input
            name="username"
            type_="string"
            placeholder="username"
            value=state.username
            onChange=(
              event => dispatch(UpdateUsername(eventToValue(event)))
            )
          />
        </div>
        <div>
          <label> (ReasonReact.string("Password:")) </label>
          <input
            name="password"
            type_="password"
            placeholder="password"
            value=state.password
            onChange=(event => dispatch(UpdatePassword(eventToValue(event))))
          />
        </div>
        <div>
          <button onClick=(event => {ReactEvent.Synthetic.preventDefault(event); dispatch(Submit)})>
            (ReasonReact.string("Submit"))
          </button>
          <button onClick=(event => {ReactEvent.Synthetic.preventDefault(event); dispatch(Clear)})>
            (ReasonReact.string("Clear"))
          </button>
        </div>
      </fieldset>
    </form>
    (
      state.show ?
        <div>
          (
            ReasonReact.string(
              "Show form state username: \""
              ++ state.username
              ++ "\", password: \""
              ++ state.password
              ++ "\".",
            )
          )
        </div> :
        ReasonReact.null
    )
  </div>;

In the username text input, we get the value of the input box from state.username.

value=state.username

Everytime the user changes the value of the username input, the component sends
a message to update the username field of its state.

onChange=(
  event => dispatch(UpdateUsername(eventToValue(event)))
)

The password input box behaves exactly the same way.

value=state.password
onChange=(event => dispatch(UpdatePassword(eventToValue(event))))

We have two buttons, Submit and Clear. Since these buttons are located in a form
they will send a request to the server. However, we do not want to do this yet.
We prevent it with ReactEvent.Synthetic.preventDefault(event). Then they
dispatch there actions. The Submit action sets show to true (in a real
login form it would validate the values then send them to the server) and Clear
sets the state to the default value.

<button onClick=(event => {ReactEvent.Synthetic.preventDefault(event); dispatch(Submit)})>
  (ReasonReact.string("Submit"))
</button>
<button onClick=(event => {ReactEvent.Synthetic.preventDefault(event); dispatch(Clear)})>
  (ReasonReact.string("Clear"))
</button>

Below the form there is a piece of logic that renders the state when show
is set to true. If you click Cancel it will hide this because it sets
show to false. Notice that it uses the ternary operator ... ? ... : ...
and in the last clause it return ReasonReact.null. This means render nothing.

(
  state.show ?
    <div>
      (
        ReasonReact.string(
        "Show form state username: \""
          ++ state.username
          ++ "\", password: \""
          ++ state.password
          ++ "\".",
        )
      )
    </div> :
    ReasonReact.null
)

Login.re is now complete. Build it and check it out in your browser.

yarn build
PORT=12345 yarn server

Final Login.re

Here is the complete Login.re file if you want to copy it and alter things.

let eventToValue = event => ReactEvent.Form.target(event)##value;

type state = {
  username: string,
  password: string,
  show: bool,
};

let defaultState = {username: "", password: "", show: false};

type action =
  | UpdateUsername(string)
  | UpdatePassword(string)
  | Submit
  | Clear;

[@react.component]
let make = () => {
  let (state, dispatch) =
    React.useReducer(
      (state, action) =>
        switch (action) {
        | UpdateUsername(username) => {...state, username}
        | UpdatePassword(password) => {...state, password}
        | Submit =>
          Js.log(state);
          {...state, show: true};
        | Clear => defaultState
        },
      defaultState,
    );

  <div>
    <form>
      <fieldset>
        <legend> (ReasonReact.string("Login Details")) </legend>
        <div>
          <label> (ReasonReact.string("Username:")) </label>
          <input
            name="username"
            type_="string"
            placeholder="username"
            value=state.username
            onChange=(
              event => dispatch(UpdateUsername(eventToValue(event)))
            )
          />
        </div>
        <div>
          <label> (ReasonReact.string("Password:")) </label>
          <input
            name="password"
            type_="password"
            placeholder="password"
            value=state.password
            onChange=(event => dispatch(UpdatePassword(eventToValue(event))))
          />
        </div>
        <div>
          <button onClick=(event => {ReactEvent.Synthetic.preventDefault(event); dispatch(Submit)})>
            (ReasonReact.string("Submit"))
          </button>
          <button onClick=(event => {ReactEvent.Synthetic.preventDefault(event); dispatch(Clear)})>
            (ReasonReact.string("Clear"))
          </button>
        </div>
      </fieldset>
    </form>
    (
      state.show ?
        <div>
          (
            ReasonReact.string(
              "Show form state username: \""
              ++ state.username
              ++ "\", password: \""
              ++ state.password
              ++ "\".",
            )
          )
        </div> :
        ReasonReact.null
    )
  </div>;
};
Discover and read more posts from James Haver
get started