In the second part of the article, we will create a straightforward React application with an authentication system. Again, I won’t get hung up on styles, so we don’t get distracted by less important things, and I will focus entirely on the authentication functionality.
Also, I assume you are familiar with React.js and Redux libraries. Otherwise, I recommend reviewing the react and redux documentation before reading further.
It’s worth noting that the approach below is just an example, and there is always a way to do it differently. However, I tried to choose the simplest way to implement authentication.
The code from the article can be found here.
Step 0. Initiate a new project
Before we start, make sure you have create-react-app
globally installed and in case you don’t, run the line below.
npm install -g create-react-app
We are now ready to create a new react application.
create-react-app react_jwt_auth
cd ./react_jwt_auth
Next, we need to install the libraries we need for this project.
Step 1. Libraries installation
npm i --save react-router-dom \
redux \
react-redux \
axios \
redux-thunk \
redux-form
Step 2. The App Component
Open the file and replace the code inside with the following:
./src/App.js
const App = () => {
return (
<div>
Hi, I'm the app!
</div>
)
}
export default App
Start the server
npm run start
Open the localhost:3000
and you should see:
Hi, I'm the app!
Step 3. Creating the header
Create a folder components
and there a file Header.js
with the following code:
./src/components/Header.js
import { NavLink } from "react-router-dom"
const Header = () => {
return (
<div>
<NavLink to="/">Main</NavLink>
<NavLink to="/signin">Signin</NavLink>
<NavLink to="/heart">Heart</NavLink>
<NavLink>Sign Out</NavLink>
</div>
)
}
export default Header;
Next, we want to add the header we just created to all pages, so let’s add it to the file:
./src/App.js
import Header from './components/Header'
const App = () => {
return (
<div>
<Header></Header>
</div>
)
}
export default App
Step 4. Connect react-router
Our routing won’t work unless we add routing to the application. Open index.js
and wrap the app component with the BrowserRouter
.
./src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; // don't forget to import BrowserRouter
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
We check that everything works, start the server, and go to localhost: 3000
run npm run
You should now see our ugly header.
Step 5. Scaffolding
Now let’s create some blank pages that we will fill in in the following chapters. Create the following files with components inside:
src/components/Welcome.js
- the public page will be the application’s root:
const Welcome = () => {
return (
<div>
Welcome page! You need to sign in first!
</div>
)
}
export default Welcome
src/components/Signin.js
- the login page:
const Signin = () => {
return (
<div>
Signin page!
</div>
)
}
export default Signin
src/components/Heart.js
- the private page, access to which only authorized users have:
const Heart = () => {
return (
<div>
Heart page!
</div>
)
}
export default Heart
Next, we want to include these pages in our routing. All of them will be displayed inside the App
component, so the Header
is available on all pages.
Open App.js
and update it with the code snippet below:
./src/App.js
import Header from './components/Header'
const App = ({ children }) => {
return (
<div>
<Header></Header>
{children}
</div>
)
}
export default App
Let’s also add routing functionality to
./src/index.js
// Add the following import to the top of the file.
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Welcome from './components/Welcome';
import Signin from './components/Signin';
import Heart from './components/Heart';
// Replace <App/> component with the following code snippet
// it works with "react-router-dom": "^6.6.1" if you have an older version, you may need to change the code below
<App>
<Routes>
<Route path ="/" element={<Welcome />} />
<Route path ="signin" element={<Signin />} />
<Route path ="heart" element={<Heart />} />
</Routes>
</App>
Finally, start the server and check localhost:3000
so we can navigate over pages.
npm run start
Step 6. Including Redux
It’s time to add redux to the app. Create folder reducers
and another folder inside reducers/auth
with file index.js
and paste:
./src/reducers/auth/index.js
// This is a simple state object where we will track whether the user is authenticated, and `errorMessage` is for errors we want to display to the user.
const INITIAL_STATE = {
authenticated: '',
errorMessage: ''
}
const auth = (state = INITIAL_STATE, action) => {
return state
}
export default auth
Then add another file where we will include all the reducers and paste:
./src/reducers/index.js
import { combineReducers } from "redux";
import auth from './auth'
export default combineReducers({
auth
})
After we want to add a redux store to the application, to do this, open the file and update it with the following code snippet:
./src/index.js
// Add the following imports to the top of the file
import { Provider } from 'react-redux';
import reducers from './reducers';
import { applyMiddleware, createStore } from 'redux';
import reduxThunk from 'redux-thunk';
// rest imports ...
const store = createStore(
reducers,
{}, // initial state
applyMiddleware(reduxThunk)
)
const store = createStore(
reducers,
{}, // initial state
applyMiddleware(reduxThunk)
)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App>
<Routes>
<Route path ="/" element={<Welcome />} />
<Route path ="signin" element={<Signin />} />
<Route path ="heart" element={<Heart />} />
</Routes>
</App>
</BrowserRouter>
</Provider>
</React.StrictMode>
);
Start the server as usual, and make sure you don’t have any errors.
npm run start
Step 7. ReduxForm and User Sign in
Let’s start creating an action that will make an API call to the server API to get the JWT token needed for authentication and store it in local storage.
We want to have two types, one for successful login and one for failed authentication.
Create the following file and paste the code snippet below:
src/actions/types.js
export const AUTH_USER = 'AUTH_USER'
export const AUTH_ERROR = 'AUTH_ERROR'
Then create the following folder and file for the signin action
and paste the code snippet below:
src/actions/index.js
import axios from 'axios'
import { AUTH_ERROR, AUTH_USER } from './types'
const signin = (formProps, callback) => async dispatch => {
try {
const response = await axios.post('http://localhost:3001/user_token', {
auth: formProps
})
dispatch({ type: AUTH_USER, payload: response.data.jwt })
localStorage.setItem('token', response.data.jwt)
callback() // after a successful login, we may want to redirect the user to a specific page, so we need a callback function
} catch {
dispatch({ type: AUTH_ERROR, payload: 'Email or password is incorrect!' })
}
}
export {
signin
}
Next, update the auth reducer to update the state of both failed and successful logins:
src/reducers/auth/index.js
import { AUTH_ERROR, AUTH_USER } from "../../actions/types";
const INITIAL_STATE = {
authenticated: '',
errorMessage: ''
}
const auth = (state = INITIAL_STATE, action) => {
switch (action.type) {
case AUTH_USER:
return { ...state, authenticated: action.payload }
case AUTH_ERROR:
return { ...state, errorMessage: action.payload }
default:
return state
}
}
export default auth
Also, since we want to use a redux-form in our application, let’s add it to the reducers:
src/reducers/index.js
import { combineReducers } from "redux";
import { reducer as reducerForm } from "redux-form";
import auth from './auth'
export default combineReducers({
auth,
form: reducerForm
})
Finally, we need to update the Signin
component to sign in and display errors.
src/components/Signin.js
import { connect, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { compose } from 'redux'
import { Field, reduxForm } from 'redux-form'
import { signin } from '../actions'
const Signin = ({ handleSubmit, pristine, submitting, signin }) => {
const navigate = useNavigate()
const errorMessage = useSelector(({ auth: { errorMessage } }) => errorMessage)
const onSubmit = (formProps) => {
signin(formProps, () => { navigate('/heart') })
}
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset>
<label>Email</label>
<Field
name="email"
type="text"
component="input" />
</fieldset>
<fieldset>
<label>Password</label>
<Field
name="password"
type="password"
component="input" />
</fieldset>
<div>{errorMessage}</div>
<div>
<button type="submit" disabled={pristine || submitting}>
Submit
</button>
</div>
</form>
</div>
)
}
export default compose(
connect(null, { signin }),
reduxForm({ form: 'signin' })
)(Signin)
Every time we reload the page, the state of our application is lost, which means we need to log in after every page reload. To fix this, let’s set the initial state from localStorage. Replace store
with the code snippet below:
src/index.js
const store = createStore(
reducers,
{
auth: {
authenticated: localStorage.getItem('token') // every time we reload the page we get the token from localStorage
}
},
applyMiddleware(reduxThunk)
)
Finally, let’s start the rails server, and the react server to test what we’ve done so far. Since both applications use port 3000
, we can switch the rails server to 3001
.
rails s -p 3001 # run it in the rails application directory
npm run start # run it in react app directory
Nothing special is currently happening. We update the state of the react app on successful or unsuccessful login and redirect the user to the /heart
route.
Step 8. Add HOC to secure private routes
We want to show some components to an unauthorized user (Welcome or Signin), but we don’t show some components (Heart).
Let’s create a High Order Component (HOC) to render the components conditionally.
Create a file and paste the code snippet below:
src/components/withAuth.js
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
const withAuth = (Component) => ({ ...props }) => {
const navigate = useNavigate()
const authenticated = useSelector(({ auth: { authenticated } }) => authenticated);
// every time the authenticated property changes, we check if the user is authenticated or not
// if not authenticated, the user is redirected to the main page
useEffect(() => {
if (!authenticated) navigate('/')
}, [authenticated, navigate])
return authenticated && <Component {...props} />;
};
export default withAuth;
Use withAuth
HOC for our private Heart
component.
Open the file and paste:
Use withAuth
HOC for our private Heart
component.
Open the file and paste:
src/components/Heart.js
import withAuth from "./withAuth"
const Heart = () => {
return (
<div>
Heart page!
</div>
)
}
export default Heart
export default withAuth(Heart) // when we want to make a component protected, we need to wrap it - withAuth(someComponent)
Step 9. Logout and update the Header
To log out, we need to add another action called logout
and export it. First, we remove the token from localStorage and then update the application state so that the “authenticated” property is set to an empty string.
Update the file below with one more action:
src/actions/index.js
const signout = () => {
localStorage.removeItem('token') // remove the token from the localStorage
return {
type: AUTH_USER,
payload: '' // the {authenticated} property is set to an empty string
}
}
export {
signin,
signout // don't forget to export the new action
}
Finally, let’s update the Header component so that we don’t display the Heart and Sign Out links for unauthenticated users, and by clicking the Sign Out link, we trigger the signout
action.
src/components/Header.js
import { connect, useSelector } from "react-redux";
import { NavLink } from "react-router-dom"
import { signout } from '../actions'
const Header = ({ signout }) => {
const authenticated = useSelector(({ auth: { authenticated } }) => authenticated)
if (authenticated) {
return (
<div>
<NavLink to="/">Main</NavLink>
<NavLink to="/heart">Heart</NavLink>
<NavLink onClick="{signout}">Sign Out</NavLink>
</div>
)
} else {
return (
<div>
<NavLink to="/">Main</NavLink>
<NavLink to="/signin">Signin</NavLink>
</div>
)
}
}
// with connect we are injecting {signout} action to the Header component
export default connect(
null,
{ signout } // injecting {signout} action
)(Header);
Start the rails and react servers to test the app’s authentication.
rails s -p 3001 # run it in the rails app directory
npm run start # run it in the react app directory
Summary
It’s all for today, my friends. Finally, we have both a backend and a frontend that can work together. I hope the authentication flow is much clearer now. See you in the following articles :)
The code from the article can be found here.