Signing up and signing in are parts of our indispensable web routine and if we plan on using React for creating complex web applications, we need to know how we are going to approach authentication in React.
We know by now, React handles communicating with our backend whatever language it uses, using APIs. There are many authentication methods available for APIs, one of which is JWT and that's what we are going to use to build today!
Build What?
Since, we are talking authentication, obviously we are going to build a sign up and a sign in page that enables users to authenticate, but on top of that, we will add a touch of fun by building a dashboard that tells us how awesome we are in clear percentages!
What's The Plan?
We want to have 3 pages:
- Sign Up
- Sign In
- Dashboard
The Dashboard page will be restricted to signed in users only. Once we sign up or sign in we will receive a token that we can use to send in our headers with any request that is restricted to signed in users.
I am going to use a simple local Node.js API that I created specifically to act as my endpoint, feel free to use it, as well, if you don't feel like creating your own:
App
After running our famous npx create-react-app .
in our folder, we are going to start by installing our router and running npm install react-router-dom
. We know for sure we have multiple pages so we need to set their routes up in our App.js file. If you need a refresher on how the router works, you can check Inspiration Of The Day: React Router
import React from "react";
import "./App.css";
import {BrowserRouter, Switch, Route} from "react-router-dom";
class App extends React.Component {
render() {
return (
<BrowserRouter basename={process.env.PUBLIC_URL}>
<div className="app">
<Switch>
<Route path="/" exact />
<Route path="/signup" />
<Route path="/signin" />
<Route path="/dashboard" />
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;
SignUp
Now let's create our pages folder and add to it our SignUp folder to start creating our page!
We will create a simple form that accepts a name, email and password. We will make all our inputs controlled components that reflect their values in their states. We will also add an error state for any error we receive from our requests.
import React from "react";
import "./SignUp.css";
class SignUp extends React.Component {
constructor() {
super();
this.state = {
name: "",
email: "",
password: "",
error: "",
};
}
handleInputChange = (event) => {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value,
});
};
render() {
let error = "";
if (this.state.error !== "") {
error = <div className="error">{this.state.error}</div>;
}
return (
<div className="signup">
<div className="header-image"></div>
<h1 className="header-title">Sign Up</h1>
{error}
<form>
<div className="form-group">
<label>Name</label>
<input
type="text"
name="name"
value={this.state.name}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label>Email</label>
<input
type="text"
name="email"
value={this.state.email}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
name="password"
value={this.state.password}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<input type="submit" value="Sign Up" />
</div>
</form>
</div>
);
}
}
export default SignUp;
Instead of creating a separate method for handling each input we used the handleInputChange
method to update all our states for us dynamically using the target name and value in the event.
What we have to handle next is our form submission and that made me realize, I don't want to handle all the token logic in the SignUp page. We will need a separate service that we can call, to communicate with our endpoint and set our token in our localStorage
for us. Let's start creating our Auth service!
Auth
A new addition to our folder structure would be a services folder. This will contain our Auth class and it will handle for us the token exchange and keeping.
Our first method in our class would be for signing up. It would accept a name, email and password, send them to the endpoint in our Node.js app and receive a token or an error message in return depending on the validity of our credentials. After processing our data, we will still return it as we will return the whole promise to be further processed in our SignUp page to check for errors.
class Auth {
signUp(name, email, password) {
return fetch(process.env.REACT_APP_ENDPOINT_BASEURL + "api/users/signup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: name,
email: email,
password: password,
}),
})
.then((response) => {
return response.json();
})
.then((data) => {
if (data.auth) {
localStorage.setItem("token", data.token);
}
return data;
})
.catch((error) => {
console.error(error);
});
}
}
export default new Auth();
I saved my local endpoint base URL in an environment variable to be able to switch it easily later.
In a successful response we receive a JSON object with an auth attribute indicating whether the authentication happened or not and the token we are going to use.
{
"auth":true,
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmMmE4NDgyZTM4ZDZhNmQ0MWE2NDlmNCIsImlhdCI6MTU5NjYyMTk1NCwiZXhwIjoxNTk2NzA4MzU0fQ.ad3E6QL2NbUa3Dh4gkJxZyY-1qZ5nUZNM_eQ2GDz8u8"
}
SignUp
Great! now we have our Auth service ready, all we need to do is import it and call it in our method while handling our submit event.
handleSubmit = (event) => {
event.preventDefault();
Auth.signUp(this.state.name, this.state.email, this.state.password).then(
(data) => {
if (!data.auth) {
this.setState({ error: data.msg });
} else {
this.props.history.push("/dashboard");
window.location.reload();
}
}
);
};
Our Auth service is returning a promise as agreed, so the first thing we check is our auth flag to display the error if exists and if we're all good we would finally be allowed to view our dashboard so we will use the React Router magic feature to navigate to our dashboard by pushing its route to our history prop and reloading our window.
We will just need to add our event to our form
<form onSubmit={this.handleSubmit}>
And our component in our route in App.js
<Route path="/signup" component={SignUp} />
Perfect! It's working! Now it's CSS time, let's add our signup.png in our SignUp folder and add our CSS to SignUp.css
.signup {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.signup .header-image {
background-image: url("./signup.png");
width: 30%;
height: 30%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
.header-title {
text-shadow: 1px 1px 1px #ff564f;
}
And add some general CSS to App.css
@import url("https://fonts.googleapis.com/css2?family=Sniglet&display=swap");
* {
box-sizing: border-box;
font-family: "Sniglet", cursive;
}
html,
body,
#root,
.app {
height: 100%;
}
body {
padding: 0;
margin: 0;
}
.app {
background-color: #dae0ec;
color: #324a58;
}
form {
width: 30%;
}
form label {
display: block;
width: 100%;
margin-bottom: 0.5rem;
}
form input {
padding: 0.5rem;
border: none;
border-radius: 5px;
width: 100%;
margin-bottom: 1rem;
}
form input[type="submit"] {
background-color: #2568ef;
color: white;
box-shadow: 0 0 3px 1px #ffe7e6;
}
Looking good!
SignIn
Our SignIn page would be pretty similar to our SignUp page so that would be a great practice for us to wrap our head around the whole process and review it one more time.
We will start this time by adding a signin
method in our Auth service where it will send our email and password to be verified in the signin endpoint and saves our token in localStorage
for us.
signIn(email, password) {
return fetch(process.env.REACT_APP_ENDPOINT_BASEURL + "api/users/signin", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
password: password,
}),
})
.then((response) => {
return response.json();
})
.then((data) => {
if (data.auth) {
localStorage.setItem("token", data.token);
}
return data;
})
.catch((error) => {
console.error(error);
});
}
After that, we will create our SignIn folder with our SignIn page and add to it a simple form that accepts an email and password. We will make our inputs controlled by adding the handleInputChange
and we will call our Auth service in the handleSubmit
method and process the response.
import React from "react";
import "./SignIn.css";
import Auth from "../../services/Auth";
class SignIn extends React.Component {
constructor() {
super();
this.state = {
email: "",
password: "",
error: "",
};
}
handleInputChange = (event) => {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value,
});
};
handleSubmit = (event) => {
event.preventDefault();
Auth.signIn(this.state.email, this.state.password).then((data) => {
if (!data.auth) {
this.setState({ error: data.msg });
} else {
this.props.history.push("/dashboard");
window.location.reload();
}
});
};
render() {
let error = "";
if (this.state.error !== "") {
error = <div className="error">{this.state.error}</div>;
}
return (
<div className="signin">
<div className="header-image"></div>
<h1 className="header-title">Sign Up</h1>
{error}
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label>Email</label>
<input
type="text"
name="email"
value={this.state.email}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
name="password"
value={this.state.password}
onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<input type="submit" value="Sign In" />
</div>
</form>
</div>
);
}
}
export default SignIn;
Good! Now let's add signin.png to the SignIn folder and CSS to SignIn.css!
.signin {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.signin .header-image {
background-image: url("./signin.png");
width: 30%;
height: 30%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
.header-title {
text-shadow: 1px 1px 1px #ff564f;
}
And our component in our route in App.js
<Route path="/signin" component={SignIn} />
We have a functional Sign In page!
Chart
To display our metrics in the dashboard, we are going to need a Chart, so let's build our Chart component first before getting carried away with our dashboard!
I chose the canvasjs charts to integrate in the app. They seemed easy enough to use for me, so let's create our components folder, add to it the Chart folder and get started!
I dowloaded the canvasjs.react.js and canvasjs.min.js files as instructed in the tutorial and placed them in my Chart folder. We are going to work with a pie chart, so I followed the straight forward instructions needed for that, customized it to my own taste and defined my own metrics of awesome!
import React from "react";
import CanvasJSReact from "./canvasjs.react";
var CanvasJSChart = CanvasJSReact.CanvasJSChart;
class Chart extends React.Component {
render() {
const options = {
animationEnabled: true,
backgroundColor: "#dae0ec",
exportEnabled: false,
data: [
{
type: "pie",
startAngle: 75,
toolTipContent: "<b>{label}</b>: {y}%",
legendText: "{label}",
indexLabelFontSize: 16,
indexLabel: "You are {y}% {label}!",
dataPoints: [
{ y: this.props.metrics.breathtaking, label: "Breathtaking" },
{ y: this.props.metrics.awesome, label: "Awesome" },
{ y: this.props.metrics.amazeballs, label: "Amazeballs" },
{ y: this.props.metrics.phenomenal, label: "Phenomenal" },
{ y: this.props.metrics.mindblowing, label: "Mind-Blowing" },
],
},
],
};
return <CanvasJSChart options={options} />;
}
}
export default Chart;
We will receive our percentage of the pie chart as a metrics prop, so our chart is done for now!
Dashboard
It's time to build our Dashboard page! In our Dashboard we want to display our metrics and want to be able to sign out.
We will want to call our endpoint to get our metrics data and send them to our chart so we will need the help of our Auth service once again.
In our Auth service let's add a getDashboard
method. We will use this method to retrieve our saved token and use it to construct our header to be authorized to retrieve the information we need from our backend.
getDashboard() {
return fetch(
process.env.REACT_APP_ENDPOINT_BASEURL + "api/users/dashboard",
{
method: "GET",
headers: {
"x-access-token": localStorage.getItem("toke"),
},
}
)
.then((response) => {
return response.json();
})
.then((data) => {
return data;
})
.catch((error) => {
console.error(error);
});
}
In our Dashboard page, we want our metrics lo load first thing, so we will call this method in componentDidMount
and use the returned data to set the values of our metrics.
import React from "react";
import "./Dashboard.css";
import Chart from "../../components/Chart/Chart";
import Auth from "../../services/Auth";
class Dashboard extends React.Component {
constructor() {
super();
this.state = {
metrics: {
breathtaking: 18,
awesome: 49,
amazeballs: 9,
phenomenal: 5,
mindblowing: 19,
},
};
}
componentDidMount() {
Auth.getDashboard().then((data) => {
if (data.success) {
this.setState({ metrics: data.metrics });
}
});
}
render() {
return (
<div className="dashboard">
<div className="signout">Sign Out?</div>
<div className="header-image"></div>
<h1 className="header-title">Metrics Of Awesome!</h1>
<div className="chart">
<Chart metrics={this.state.metrics} />
</div>
</div>
);
}
}
export default Dashboard;
Amazing! Let's add CSS in Dashboard.css!
.dashboard {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.dashboard .header-image {
background-image: url("./dashboard.png");
width: 30%;
height: 30%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
.header-title {
text-shadow: 1px 1px 1px #ff564f;
}
.dashboard .signout {
position: absolute;
top: 0;
right: 0;
margin-right: 1rem;
margin-top: 1rem;
color: #ea462d;
font-weight: 600;
cursor: pointer;
}
.dashboard .chart {
width: 100%;
}
And we shouldn't forget to update our route in App.js with our component
<Route path="/dashboard" component={Dashboard} />
Now, we have to handle two more things signing out and since users who are not signed in are restricted from accessing the dashboard, we need to check for that, as well.
In our Auth service, let's create both methods and see how we are going to use them!
signedIn() {
const token = localStorage.getItem("token");
if (token) {
return true;
}
return false;
}
signOut() {
localStorage.removeItem("token");
}
In our signedIn method, we will just check the token existence and return a flag accordingly.
In signOut, all we're going to do in that method is clear the token we saved.
Let's move to our Dashboard page and apply those functions! We'll add our handleSignOut method
handleSignOut = () => {
Auth.signOut();
};
And attach it to the Sign Out label!
<div className="signout" onClick={this.handleSignOut}>
Sign Out?
</div>
And for our sign in check, we will go all the way up to componentDidMount
and redirect to the signin route if the token was missing
componentDidMount() {
if (!Auth.signedIn()) {
this.props.history.push("/signin");
window.location.reload();
}
Auth.getDashboard().then((data) => {
if (data.success) {
this.setState({ metrics: data.metrics });
}
});
}
Awesome!
The code can be found HERE
By this metrics of awesome, I shall end my seventh baby step towards React greatness, until we meet in another one.
Any feedback or advice is always welcome. Reach out to me here, on Twitter, there and everywhere!