The general idea is that you will save the current user’s information to the Redux store for easy access across your app. You will also save the JWT (JSON Web Token) associated with the user to localStorage so that their login can persist between sessions unless they explicitly logout.
This tutorial assumes you have the three following Rails API routes set up:
POST to /users > POST to /users > POST to /users
These three routes handle the three essential parts of authentication — when a user creates an account, when a user logs into that account, and when a logged-in user revisits your web app. I will go in that order, although the final part (handling when a user revisits your app) is where JWT’s usefulness shines.
Let’s get started!
POST to /users > POST to /users ### 1. User Signs Up (POST to /users)
When a new user visits your app, you may want them to sign up for an account. You will essentially run a standard POST request; you won’t have to do anything fancy to get this up and running.
If set up properly, your backend will create the user instance, salt the password using BCrypt, and then return an object with a user key and a jwt key. This object is the important part to Auth. You’ll see it later in this tutorial, but we will essentially take the user object and save it to your Redux store, then take the token associated with the user and save it to localStorage.
The steps for signing up new users and automatically logging them in are as follows.
In your React App, you’ll want a form which, upon submission, will run your fetch in your actions.js
file. You will be making use of Redux’s thunk here, so make sure you have it installed.
Create a controlled component that is a form for creating a new user. As an example, it may look like this:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {userPostFetch} from '../redux/actions';
class Signup extends Component {
state = {
username: "",
password: "",
avatar: "",
bio: ""
}
handleChange = event => {
this.setState({
[event.target.name]: event.target.value
});
}
handleSubmit = event => {
event.preventDefault()
this.props.userPostFetch(this.state)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<h1>Sign Up For An Account</h1>
<label>Username</label>
<input
name='username'
placeholder='Username'
value={this.state.username}
onChange={this.handleChange}
/><br/>
<label>Password</label>
<input
type='password'
name='password'
placeholder='Password'
value={this.state.password}
onChange={this.handleChange}
/><br/>
<label>Avatar</label>
<input
name='avatar'
placeholder='Avatar (URL)'
value={this.state.avatar}
onChange={this.handleChange}
/><br/>
<label>Bio</label>
<textarea
name='bio'
placeholder='Bio'
value={this.state.bio}
onChange={this.handleChange}
/><br/>
<input type='submit'/>
</form>
)
}
}
const mapDispatchToProps = dispatch => ({
userPostFetch: userInfo => dispatch(userPostFetch(userInfo))
})
export default connect(null, mapDispatchToProps)(Signup);
Your component does not have to look exactly like this — the important part is the handleSubmit function.
Note where some unknown function named userPostFetch
is being imported from actions.js
and then added as a prop to the component using mapDispatchToProps
. You can see above that this prop is invoked upon submission of the form. This will be the function that handles the fetch itself, as well as saving the user object to the Redux store and adding the token to localStorage. Next, we will write this function.
In your actions.js
file, it will look something like this:
export const userPostFetch = user => {
return dispatch => {
return fetch("http://localhost:3000/api/v1/users", {
method: "POST",
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({user})
})
.then(resp => resp.json())
.then(data => {
if (data.message) {
// Here you should have logic to handle invalid creation of a user.
// This assumes your Rails API will return a JSON object with a key of
// 'message' if there is an error with creating the user, i.e. invalid username
} else {
localStorage.setItem("token", data.jwt)
dispatch(loginUser(data.user))
}
})
}
}
const loginUser = userObj => ({
type: 'LOGIN_USER',
payload: userObj
})
Note the two separate functions: userPostFetch
and loginUser
. The function userPostFetch
sends the user’s info to your backend to be verified. Upon success, it is expecting a response of a JSON object that looks like this:
{
user: {
username: "ImANewUser",
avatar: "https://robohash.org/imanewuser.png",
bio: "A new user to the app."
},
jwt: "aaaaaaa.bbbbbbbb.ccccccc"
}
In userPostFetch
, this is what we named ‘data’ in the 2nd ‘then’ statement.
With the code we’ve written in our userPostFetch
function, localStorage.setItem(“token”, data.jwt)
will save the token (“aaaaaaa.bbbbbbbb.ccccccc”
) to our user’s localStorage. This will be used later when we are persisting a user’s login between sessions.
To check that the token was saved successfully, run localStorage.token
or localStorage.getItem(“token”)
in your console.
As for the user object, we see here that dispatch(loginUser(data.user))
is being ran. Presumably, your reducer will take the user object ({username: “ImANewUser”}
) and save it to your Redux store. This will make it easy for any component in your React App to know who the current user is.
As an example, here is my reducer:
const initialState = {
currentUser: {}
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'LOGIN_USER':
return {...state, currentUser: action.payload}
default:
return state;
}
}
Here, the user object (action.payload
) is being saved to the state under the key of currentUser. If you have Redux DevTools installed, you can check it after the successful creation of your user. You should see the user object.
You may notice here that I have a key of “reducer” that you might not have. This is because I have multiple reducers and named one of them “reducer”.
POST to /users > POST to /users
That’s it for signing up a new user. Next, we’ll check out how to log in an existing user.
Logging in a user is very similar to the signup process, except you are sending only the login credentials to the backend. The backend will handle validating the user and then sending back the same object from sign up — an object with a user key and jwt key. Once again, you’ll save the user object to the Redux store and save the token to localStorage.
If you have a component dedicated to logging in, it will look similar to your Signup component with one major difference — it will be importing a different function from your actions.js
file. It might look something like this:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {userLoginFetch} from '../redux/actions';
class Login extends Component {
state = {
username: "",
password: ""
}
handleChange = event => {
this.setState({
[event.target.name]: event.target.value
});
}
handleSubmit = event => {
event.preventDefault()
this.props.userLoginFetch(this.state)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<h1>Login</h1>
<label>Username</label>
<input
name='username'
placeholder='Username'
value={this.state.username}
onChange={this.handleChange}
/><br/>
<label>Password</label>
<input
type='password'
name='password'
placeholder='Password'
value={this.state.password}
onChange={this.handleChange}
/><br/>
<input type='submit'/>
</form>
)
}
}
const mapDispatchToProps = dispatch => ({
userLoginFetch: userInfo => dispatch(userLoginFetch(userInfo))
})
export default connect(null, mapDispatchToProps)(Login);
Note that the only thing that changed about the form itself was the removal of the avatar and bio input fields.
We haven’t yet written the userLoginFetch
function, but again, its appearance is similar to the fetch that handled sign up. See below:
export const userLoginFetch = user => {
return dispatch => {
return fetch("http://localhost:3000/api/v1/login", {
method: "POST",
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({user})
})
.then(resp => resp.json())
.then(data => {
if (data.message) {
// Here you should have logic to handle invalid login credentials.
// This assumes your Rails API will return a JSON object with a key of
// 'message' if there is an error
} else {
localStorage.setItem("token", data.jwt)
dispatch(loginUser(data.user))
}
})
}
}
Note here that we are reusing the loginUser
action in order to save our user object to our state.
Surprisingly, that’s it for logging a user in! When a user’s object is saved to the state and their token is saved to localStorage, you can consider your user logged in.
Now let’s do the third and final part: persisting your user’s login between sessions.
The point of saving a token to localStorage is to persist a login. When your user revisits your site, you want them to feel as if they are continuing their session from before.
Remember though that the token saved into localStorage is just a string. It in itself does not equal a logged-in user. You as the developer must take the token and translate it into a persisting login.
To do this, you will want to run your fetch (GET to /profile) every time your app is accessed if the user has a token saved into their localStorage. Running this logic in componentDidMount in your App component is a good choice, as it will definitely run when your app is accessed.
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import {connect} from 'react-redux';
import {getProfileFetch} from './redux/actions';
import Signup from './components/Signup';
import Login from './components/Login';
class App extends Component {
componentDidMount = () => {
this.props.getProfileFetch()
}
render() {
return (
<div>
<Switch>
<Route path="/signup" component={Signup}/>
<Route path="/login" component={Login}/>
</Switch>
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
getProfileFetch: () => dispatch(getProfileFetch())
})
export default connect(null, mapDispatchToProps)(App);
Your App component won’t look exactly like this (you can especially ignore the Switch and Routes parts), but the key part here is the getProfileFetch function being given as a prop to App and then invoked in componentDidMount.
We’re importing a function from actions.js
called getProfileFetch
, which is ran immediately when the App component mounts.
What getProfileFetch
will do is run a standard GET request, except with an Authorization header with the token, which you handle in your actions.js
file. Your backend should be set up to receive the token, decode it, and then return its associated user object. You then save this to the Redux store as usual. You already have the token saved to localStorage, so you don’t have to worry about it.
The function will look something like this:
export const getProfileFetch = () => {
return dispatch => {
const token = localStorage.token;
if (token) {
return fetch("http://localhost:3000/api/v1/profile", {
method: "GET",
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'Authorization': `Bearer ${token}`
}
})
.then(resp => resp.json())
.then(data => {
if (data.message) {
// An error will occur if the token is invalid.
// If this happens, you may want to remove the invalid token.
localStorage.removeItem("token")
} else {
dispatch(loginUser(data.user))
}
})
}
}
}
The function getProfileFetch
first checks if there is a token saved into localStorage before attempting to persist a login. This way, you won’t run an unnecessary fetch.
Note here again that we are reusing the loginUser
action and that we are not re-saving the token. The user already has it in their localStorage, so there’s no need to save it again.
And there you have it — you have functioning authorization!
As you test your app, you’ll notice that currently the only way to log out is to clear the JWT token in your localStorage by typing localStorage.removeItem(“token”)
into your console and pressing refresh to clear the Redux store. We should create a button for your clients to log themselves out.
Somewhere in your app, you’ll need a logout button which will do the above. While I wouldn’t place this randomly in the App.js
file, here is an example of a logout button in App.js
for sake of example.
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import {connect} from 'react-redux';
import {getProfileFetch, logoutUser} from './redux/actions';
import Signup from './components/Signup';
import Login from './components/Login';
class App extends Component {
componentDidMount = () => {
this.props.getProfileFetch()
}
handleClick = event => {
event.preventDefault()
// Remove the token from localStorage
localStorage.removeItem("token")
// Remove the user object from the Redux store
this.props.logoutUser()
}
render() {
return (
<div>
<Switch>
<Route path="/signup" component={Signup}/>
<Route path="/login" component={Login}/>
</Switch>
{this.props.currentUser.username
? <button onClick={this.handleClick}>Log Out</button>
: null
}
</div>
);
}
}
const mapStateToProps = state => ({
currentUser: state.reducer.currentUser
})
const mapDispatchToProps = dispatch => ({
getProfileFetch: () => dispatch(getProfileFetch()),
logoutUser: () => dispatch(logoutUser())
})
export default connect(mapStateToProps, mapDispatchToProps)(App);
You should notice a few new things: for one, there is a new action being imported called logoutUser
, which we will write shortly. We also now have mapStateToProps being used in order for the App component to receive a prop called currentUser.
POST to /users
There is now also a ternary operator which checks if the currentUser prop received from the Redux store has a username key (as in, if the currentUser object is empty or not). If it does, it renders a ‘Log Out’ button, which will invokelogoutUser
when clicked. It will also remove the token from localStorage.
logoutUser
will just be a simple action:
export const logoutUser = () => ({
type: 'LOGOUT_USER'
})
This action will do the following in the reducer, replacing the currentUser with an empty object:
const initialState = {
currentUser: {}
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'LOGIN_USER':
return {...state, currentUser: action.payload}
case 'LOGOUT_USER':
return {...state, currentUser: {} }
default:
return state;
}
}
And there you have it! You should see the button appearing and disappearing when you log in and log out.
You may have followed the above and are questioning if your authorization is working. You can test it by signing up, logging in, pressing refresh, and logging out. If everything works as it should, you should see the user object saved to your Redux store under the key of currentUser using your Redux DevTools and a token saved to your localStorage which you can view in the console.
As for where to go from here, you can run checks in your components to essentially kick a user out if they are not logged in. You can do this with Redirect
from react-router-dom
or the push
function from connected-react-router
. These are the resources I prefer to use.
You can also now pass your currentUser object from your Redux store to any component in your app, as we saw earlier. For example, if you wanted your Navbar to show the user’s avatar, you could use mapStateToProps
to pass the currentUser object to the Navbar and then render the avatar.
Thanks for reading ❤
If you liked this post, share it with all of your programming buddies!
#reactjs #redux