State¶
Now that we have all the bits in place, we can start adding some interactive functionality. The core principle in React is that each component has a state and the contents of that state flows down into the displayed HTML. When the user interacts with the interface, all changes are sent via events back up to the component, which then updates the state to trigger a re-rendering. This differs from Ember, where, for example, state can flow both directions, as we made use of with the {{input}}
helper.
Displaying State¶
The first step is to define the initial state of our component, by adding the following hook into it:
import React, { useState } from "react";
import ReactDom from "react-dom";
import "../styles/app.scss";
function Athena() {
const [email, setMail] = useState('')
const [password, setPassword] = useState('')
const [loginValid, setLoginValid] = useState(true)
...
}
In the initialState
we can specify the initial state of our component. This state can then be used when rendering the component:
render() {
...
<input type="email" name="email" placeholder="Your e-mail address" tabIndex="1" required="required" value={email} />
...
<input type="password" name="password" placeholder="Your password" tabIndex="2" required="required" value={password} />
...
}
useState
is one of ten built-in hooks, that React provides. When using hooks a component can have several states. useState return an array that always has two values. The first value is the getter, the second value is the setter-function. We can use this knowledge to profit from JavaScripts array destructuring to create our variables that store our state, as you can see above. We can pass the initial state as parameter for useState
.
In React {something}
is the equivalent of {{something}}
in handlebars and just as in handlebars, it can be used to attach variables to attributes, but also just to output text anywhere else. The difference is that in JSX the content can be any valid variable, regardless of whether it is defined on the state
, as in this case, or just a local variable, as we will use later in the tutorial. You can now try changing the initial state and you will see that the page reloads with the state displayed. Ignore the warning in the console, we will deal with it next.
User Input¶
The next step is to add the necessary functionality to implement user input. As stated above, state flows down, but only events flow back up. We thus need to attach event listeners to the input elements:
render() {
...
<input type="email" name="email" placeholder="Your e-mail address" tabIndex="1" required="required" value={email} onChange={updateEmail} />
...
<input type="password" name="password" placeholder="Your password" tabIndex="2" required="required" value={password} onChange={updatePassword} />
...
}
Unlike in Ember, where we use actions to link the template to the JavaScript code, in React, we simply attach a function to the relevant event attribute. Here we want to listen to changes on the email and password fields and then call the respective event handlers. To make this work, we need to add the two event handler functions into our component:
const updateEmail = (ev) => {
setMail(ev.target.value)
}
const updatePassword = (ev) => {
setPassword(ev.target.value)
}
This is only one way to write our functions, we could use the regular function <functionName>(params) {}
syntax, as well. Inside the event handlers you must always use the setter function we defined in setState
to update the component’s state. The function takes a single object that defines the state variables to be updated and their new values. In these two cases, we set the email
and password
values of the state to the value the user had typed into the input field (accessible via the ev.target.value
).
At this point there is no visual result of updating the state. To see an effect, we will add an event listener for the form submission:
<form onSubmit={handleLogin}>
const handleLogin = (ev) => {
ev.preventDefault();
if (email === "test@example.com" && password === "password") {
setLoginValid(true);
} else {
setLoginValid(false);
}
}
This will now handle the submit event, and prevent the default action of submission, but because we are not reacting to the change in the loginValid
state, there is no visible result. To react to this change, we update the render
method:
let errorTag = null;
if (!loginValid) {
errorTag = (
<span className="invalid-feedback">No user exists with the e-mail address {email} or the password is incorrect.</span>
);
}
return (
...
<form action="index.html" onSubmit={handleLogin}>
<div>
<label>
E-Mail Address
<input
className="form-control"
type="email"
name="email"
placeholder="Your e-mail address"
tabIndex="1"
required="required"
value={email}
onChange={updateEmail}
/>
{errorTag}
</label>
</div>
<div className="form-group">
<label>
Password
<input
className="form-control"
type="password"
name="password"
placeholder="Your password"
tabIndex="2"
required="required"
value={password}
onChange={updatePassword}
/>
{errorTag}
</label>
</div>
...
)
As you can see before the start of the return
statement, we define a new variable errorTag
, and if the loginValid
state is false, then we assigned a block of JSX to that variable. Then in the main JSX block, we can simply output the content of the errorTag
variable. If it is null
, then nothing is displayed, whereas if it is set to the given JSX value, then that is simply inserted into the output at that point. If you try the login functionality now, you will see a nice error message in the inspector.
If we wanted to just set attributes to specific values if there is an error, or if we want to actually see the error indicator, then we would use the same pattern:
let errorTag = null;
let errorLabel = null;
let errorInput = null;
if (!loginValid) {
errorTag = (
<span className="invalid-feedback">
No user exists with the e-mail address {email} or the password is
incorrect.
</span>
);
errorLabel = "text-danger";
errorInput = "form-control is-invalid";
} else {
errorLabel = null;
errorTag = null;
errorInput = "form-control"
}
return (
...
<form className="col-6" action="index.html" onSubmit={handleLogin}>
<h1>Login</h1>
<div className="form-group">
<label className={errorLabel}>
E-Mail Address
<input
className={errorInput}
type="email"
name="email"
placeholder="Your e-mail address"
tabIndex="1"
required="required"
value={email}
onChange={updateEmail}
/>
{errorTag}
</label>
</div>
<div className="form-group">
<label className={errorLabel}>
Password
<input
className={errorInput}
type="password"
name="password"
placeholder="Your password"
tabIndex="2"
required="required"
value={password}
onChange={updatePassword}
/>
{errorTag}
</label>
</div>
...
)
}
Here we define the errorLabel
and errorTag
variables and given them a value if there is an error. Then in the main JSX template, we can simply attach those to the relevant attributes. React is smart enough about this, so that if the variable is null
(no error), then the attribute is simply not output. Note, that we overrode the className
attribute for both input
elements.
Important
Whenever you update the state, the page will be rendered again will be called. It is thus important that you do the minimal amount of processing possible in component to ensure high performance. Processing should generally be done when receiving events and the results stored in the state
, then they can simply be displayed.