Introduction to ReasonReact Components
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>;
};