React + Recoil - JWT Authentication Tutorial & Example
Tutorial built with React 17.0.2 and Recoil 0.4.1
Other versions available:
- React: React 18 + Redux, React 16 + Redux, React + RxJS
- Vue: Vue 3 + Pinia, Vue.js + Vuex
- Angular: Angular 14, 10, 9, 8, 7, 6, 2/5
- Next.js: Next.js 11
- AngularJS: AngularJS
- ASP.NET Core: Blazor WebAssembly
This tutorial shows how to build a simple login application with React and Recoil that uses JWT authentication.
Example React + Recoil App
The example app is pretty minimal and contains just 2 pages to demonstrate JWT authentication in React and Recoil:
/login
- public login page with username and password fields, on submit the page sends a POST request to the API to authenticate user credentials, on success the API returns a JWT token to make authenticated requests to secure API routes./
- secure home page that displays a list of users fetched from a secure API endpoint using the JWT token received after successful login.
Recoil Overview
Recoil is a new state management library built by the Facebook team that simplifies global state management, it requires much less code than other libraries like Redux, and is easier to learn because it works just like React. The two core concepts of Recoil are atoms and selectors, an atom defines a unit (key) in the global state object of an app, and a selector is function that returns a value that is derived (computed) from atoms and/or other selectors. For more info on Recoil see https://recoiljs.org/docs/introduction/core-concepts.
Fake Backend API
The React + Recoil example app runs with a fake backend by default to enable it to run completely in the browser without a real backend API (backend-less), to switch to a real backend API you just have to remove or comment out the 2 lines below the comment // setup fake backend
located in the main index file (/src/index.jsx
). You can build your own API or hook it up with the .NET or Node.js API available (instructions below).
Code on GitHub
The example project is available on GitHub at https://github.com/cornflourblue/react-recoil-jwt-authentication-example.
Here it is in action:(See on StackBlitz at https://stackblitz.com/edit/react-recoil-jwt-authentication-example)
Run the React + Recoil JWT Example Locally
- Install Node.js and npm from https://nodejs.org.
- Download or clone the project source code from https://github.com/cornflourblue/react-recoil-jwt-authentication-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Start the application by running
npm start
from the command line in the project root folder, this will launch a browser displaying the application.
Run the React + Recoil App with a .NET API
For full details about the example .NET JWT Auth API see the post .NET 5.0 - JWT Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.
- Install the .NET SDK from https://dotnet.microsoft.com/download.
- Download or clone the project source code from https://github.com/cornflourblue/dotnet-5-jwt-authentication-api
- Start the api by running
dotnet run
from the command line in the project root folder (where the WebApi.csproj file is located), you should see the messageNow listening on: http://localhost:4000
. - Back in the React + Recoil example app, remove or comment out the 2 lines below the comment
// setup fake backend
located in the/src/index.jsx
file, then start the React + Recoil app and it should now be hooked up with the .NET API.
Run the React + Recoil App with a Node.js API
For full details about the example Node.js JWT Auth API see the post NodeJS - JWT Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.
- Download or clone the project source code from https://github.com/cornflourblue/node-jwt-authentication-api
- Start the api by running
npm start
from the command line in the project root folder, you should see the messageServer listening on port 4000
. - Back in the React + Recoil example app, remove or comment out the 2 lines below the comment
// setup fake backend
located in the/src/index.jsx
file, then start the React + Recoil app and it should now be hooked up with the Node.js API.
React + Recoil Project Structure
Create React App was used to generate the base project structure with the npx create-react-app <project name>
command, the tool is also used to build and serve the application. For more info about Create React App see https://create-react-app.dev/.
The project source (/src
) is organised into the following folders:
- _actions
Action objects that contain methods for performing actions on each content type, for example the user actions object contains methods for login, logout and fetching users. Actions encapsulate all HTTP communication with the API and recoil state update operations. - _components
React components used by pages or by other react components. - _helpers
Anything that doesn't fit into the other folders and doesn't justify having its own folder. - _state
Recoil atoms and selectors that define the global state available to the React application (the example app only contains atoms). For more info on Recoil see https://recoiljs.org/docs/introduction/core-concepts. - home
Components used only by the home page - login
Components used only by the login page
Each feature has it's own folder (home & login), other shared/common code such as actions, state, components, helpers etc are placed in folders prefixed with an underscore _
to easily differentiate them from features and to group them together at the top of the folder structure.
JavaScript files are organised with export
statements at the top so it's easy to see all exported modules when you open a file. Export statements are followed by functions and other implementation code for each JS module.
The index.js
file in each folder re-exports all of the modules from that folder so they can be imported using only the folder path instead of the full path to each module, and to enable importing multiple modules in a single import (e.g. import { Nav, PrivateRoute } from '_components';
).
The baseUrl
is set to "src"
in the jsconfig.json file to make all import statements (without a dot '.' prefix) relative to the root folder of the project, removing the need for long relative paths like import { userActions } from '../../../_actions';
.
Click any of the below links to jump down to a description of each file along with it's code:
- public
- src
- _actions
- user.actions.js
- index.js
- _components
- Nav.jsx
- PrivateRoute.jsx
- index.js
- _helpers
- fake-backend.js
- fetch-wrapper.js
- history.js
- index.js
- _state
- home
- Home.jsx
- index.js
- login
- Login.jsx
- index.js
- App.jsx
- index.css
- index.js
- _actions
- .env
- jsconfig.json
- package.json
Main Index Html File
The main index.html file is the initial page loaded by the browser that kicks everything off. Create React App (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
<!-- bootstrap css -->
<link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="app"></div>
</body>
</html>
User Actions
The user actions object returned by the useUserActions()
hook function contains methods for login, logout and fetching all users. It handles communication between the React app and the backend api for everything related to users, and also handles Recoil state update operations for users and auth atoms. HTTP requests to the API are sent with the fetch wrapper.
A React hook function is required because Recoil hook functions (e.g. useSetRecoilState
) can only be called within React components or hook functions.
import { useSetRecoilState } from 'recoil';
import { history, useFetchWrapper } from '_helpers';
import { authAtom, usersAtom } from '_state';
export { useUserActions };
function useUserActions () {
const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
const fetchWrapper = useFetchWrapper();
const setAuth = useSetRecoilState(authAtom);
const setUsers = useSetRecoilState(usersAtom);
return {
login,
logout,
getAll
}
function login(username, password) {
return fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
.then(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
setAuth(user);
// get return url from location state or default to home page
const { from } = history.location.state || { from: { pathname: '/' } };
history.push(from);
});
}
function logout() {
// remove user from local storage, set auth state to null and redirect to login page
localStorage.removeItem('user');
setAuth(null);
history.push('/login');
}
function getAll() {
return fetchWrapper.get(baseUrl).then(setUsers);
}
}
Nav Component
The nav component displays the primary bar in the example. The component gets the current auth
data from global Recoil state by calling useRecoilValue(authAtom)
and only displays the nav if the user is logged in.
The react router NavLink
component automatically adds the active
class to the active nav item so it is highlighted in the UI.
import { NavLink } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { authAtom } from '_state';
import { useUserActions } from '_actions';
function Nav() {
const auth = useRecoilValue(authAtom);
const userActions = useUserActions();
// only show nav when logged in
if (!auth) return null;
return (
<nav className="navbar navbar-expand navbar-dark bg-dark">
<div className="navbar-nav">
<NavLink exact to="/" className="nav-item nav-link">Home</NavLink>
<a onClick={userActions.logout} className="nav-item nav-link">Logout</a>
</div>
</nav>
);
}
export { Nav };
Private Route
The react private route component renders a route component if the user is logged in, if the user isn't logged in they're redirected to the /login
page with the return url in the location state property.
The current logged in (auth
) state of the user is retrieved from Recoil with a call to useRecoilValue(authAtom)
.
import { Route, Redirect } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { authAtom } from '_state';
export { PrivateRoute };
function PrivateRoute({ component: Component, ...rest }) {
const auth = useRecoilValue(authAtom);
return (
<Route {...rest} render={props => {
if (!auth) {
// not logged in so redirect to login page with the return url
return <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
}
// authorized so return component
return <Component {...props} />
}} />
);
}
Fake Backend
In order to run and test the React + Recoil app without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the React app and sends back "fake" responses. This is done by monkey patching the window.fetch()
function to return fake responses for a specific set of routes.
Monkey patching is a technique used to alter the behaviour of an existing function either to extend it or change the way it works. In JavaScript this is done by storing a reference to the original function in a variable and replacing the original function with a new custom function that (optionally) calls the original function before/after executing some custom code.
The fake backend is organised into a top level handleRoute()
function that checks the request url and method to determine how the request should be handled. For fake routes one of the below // route functions
is called, for all other routes the request is passed through to the real backend by calling the original fetch request function (realFetch(url, opts)
). Below the route functions there are // helper functions
for returning different response types and performing small tasks.
export { fakeBackend };
function fakeBackend() {
let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];
let realFetch = window.fetch;
window.fetch = function (url, opts) {
return new Promise((resolve, reject) => {
// wrap in timeout to simulate server api call
setTimeout(handleRoute, 500);
function handleRoute() {
switch (true) {
case url.endsWith('/users/authenticate') && opts.method === 'POST':
return authenticate();
case url.endsWith('/users') && opts.method === 'GET':
return getUsers();
default:
// pass through any requests not handled above
return realFetch(url, opts)
.then(response => resolve(response))
.catch(error => reject(error));
}
}
// route functions
function authenticate() {
const { username, password } = body();
const user = users.find(x => x.username === username && x.password === password);
if (!user) return error('Username or password is incorrect');
return ok({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
token: 'fake-jwt-token'
});
}
function getUsers() {
if (!isAuthenticated()) return unauthorized();
return ok(users);
}
// helper functions
function ok(body) {
resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) })
}
function unauthorized() {
resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorized' })) })
}
function error(message) {
resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) })
}
function isAuthenticated() {
return opts.headers['Authorization'] === 'Bearer fake-jwt-token';
}
function body() {
return opts.body && JSON.parse(opts.body);
}
});
}
}
Fetch Wrapper
The fetch wrapper is a lightweight wrapper around the native browser fetch()
function used to simplify the code for making HTTP requests. It returns an object with methods for get
, post
, put
and delete
requests, it automatically handles the parsing of JSON data from responses, and throws an error if the HTTP response is not successful (!response.ok
). If the response is 401 Unauthorized
or 403 Forbidden
the user is automatically logged out of the React + Recoil app.
The authHeader()
function is used to automatically add a JWT auth token to the HTTP Authorization header of the request if the user is logged in and the request is to the application API url. The current logged in (auth
) state of the user is retrieved from Recoil with a call to useRecoilState(authAtom)
, the setAuth()
function is used in the handleResponse()
function to log the user out if required.
With the fetch wrapper a POST
request can be made as simply as this: fetchWrapper.post(url, body);
. It's used in the example app by user actions.
import { useRecoilState } from 'recoil';
import { history } from '_helpers';
import { authAtom } from '_state';
export { useFetchWrapper };
function useFetchWrapper() {
const [auth, setAuth] = useRecoilState(authAtom);
return {
get: request('GET'),
post: request('POST'),
put: request('PUT'),
delete: request('DELETE')
};
function request(method) {
return (url, body) => {
const requestOptions = {
method,
headers: authHeader(url)
};
if (body) {
requestOptions.headers['Content-Type'] = 'application/json';
requestOptions.body = JSON.stringify(body);
}
return fetch(url, requestOptions).then(handleResponse);
}
}
// helper functions
function authHeader(url) {
// return auth header with jwt if user is logged in and request is to the api url
const token = auth?.token;
const isLoggedIn = !!token;
const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
if (isLoggedIn && isApiUrl) {
return { Authorization: `Bearer ${token}` };
} else {
return {};
}
}
function handleResponse(response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if ([401, 403].includes(response.status) && auth?.token) {
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
localStorage.removeItem('user');
setAuth(null);
history.push('/login');
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}
}
History
The history helper creates the browser history object used by the React + Recoil app, it is passed to the Router
component in the main index.js file and enables us to access the history object outside of react components, for example from the logout()
method of the user actions.
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export { history };
Recoil Auth State
The auth state file contains the Recoil auth
atom that is used to hold the current logged in user in global state, the auth state is updated in the user actions login and logout functions.
The example app only contains Recoil atoms at the moment, but my idea with this file structure is that any Recoil selectors that derive from the auth
atom could be placed in this file as well.
import { atom } from 'recoil';
const authAtom = atom({
key: 'auth',
// get initial state from local storage to enable user to stay logged in
default: JSON.parse(localStorage.getItem('user'))
});
export { authAtom };
Recoil Users State
The users state file contains the Recoil users
atom that is used to hold the array of fetched users in global state, the users state is updated in the user actions getAll function.
The example app only contains Recoil atoms at the moment, but my idea with this file structure is that any Recoil selectors that derive from the users
atom could be placed in this file as well.
import { atom } from 'recoil';
const usersAtom = atom({
key: 'users',
default: null
});
export { usersAtom };
Home Component
The home page is displayed after signing in to the application, it shows the signed in user's name plus a list of all users in the tutorial application. The users are loaded into Recoil state by calling userActions.getAll()
from the useEffect()
hook function, see how the users are fetched and the Recoil state is updated in user actions.
Recoil state values are retrieved for auth
and users
data with the help of the useRecoilValue()
hook function.
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { authAtom, usersAtom } from '_state';
import { useUserActions } from '_actions';
export { Home };
function Home() {
const auth = useRecoilValue(authAtom);
const users = useRecoilValue(usersAtom);
const userActions = useUserActions();
useEffect(() => {
userActions.getAll();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<h1>Hi {auth?.firstName}!</h1>
<p>You're logged in with React + Recoil & JWT!!</p>
<h3>Users from secure api end point:</h3>
{users &&
<ul>
{users.map(user =>
<li key={user.id}>{user.firstName} {user.lastName}</li>
)}
</ul>
}
{!users && <div className="spinner-border spinner-border-sm"></div>}
</div>
);
}
Login Component
The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the React + Recoil app.
Form validation rules are defined with the Yup schema validation library and passed with the formOptions
to the React Hook Form useForm()
function, for more info on Yup see https://github.com/jquense/yup.
The useForm()
hook function returns an object with methods for working with a form including registering inputs, handling form submit, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.
The onSubmit
function gets called when the form is submitted and valid, and submits the user credentials to the api by calling userActions.login()
. On successful authentication the user auth
data is stored in Recoil shared state by the login method in user actions.
The returned JSX template contains the markup for page including the form, input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('username')}
). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.
import { useEffect } from 'react';
import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
import { useRecoilValue } from 'recoil';
import { authAtom } from '_state';
import { useUserActions } from '_actions';
export { Login };
function Login({ history }) {
const auth = useRecoilValue(authAtom);
const userActions = useUserActions();
useEffect(() => {
// redirect to home if already logged in
if (auth) history.push('/');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// form validation rules
const validationSchema = Yup.object().shape({
username: Yup.string().required('Username is required'),
password: Yup.string().required('Password is required')
});
const formOptions = { resolver: yupResolver(validationSchema) };
// get functions to build form with useForm() hook
const { register, handleSubmit, setError, formState } = useForm(formOptions);
const { errors, isSubmitting } = formState;
function onSubmit({ username, password }) {
return userActions.login(username, password)
.catch(error => {
setError('apiError', { message: error });
});
}
return (
<div className="col-md-6 offset-md-3 mt-5">
<div className="alert alert-info">
Username: test<br />
Password: test
</div>
<div className="card">
<h4 className="card-header">Login</h4>
<div className="card-body">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-group">
<label>Username</label>
<input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.username?.message}</div>
</div>
<div className="form-group">
<label>Password</label>
<input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
<div className="invalid-feedback">{errors.password?.message}</div>
</div>
<button disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
Login
</button>
{errors.apiError &&
<div className="alert alert-danger mt-3 mb-0">{errors.apiError?.message}</div>
}
</form>
</div>
</div>
</div>
)
}
App Component
The App
component is the root component of the example app, it contains the outer html, main nav and routes for the application.
The /login
route is public, and the home route (/
) is secured by the private route component that uses Recoil to check if the user is logged in.
The last route (<Redirect from="*" to="/" />) is a catch-all redirect route that redirects any unmatched paths to the home page.
import { Router, Route, Switch, Redirect } from 'react-router-dom';
import { Nav, PrivateRoute } from '_components';
import { history } from '_helpers';
import { Home } from 'home';
import { Login } from 'login';
export { App };
function App() {
return (
<div className="app-container bg-light">
<Router history={history}>
<Nav />
<div className="container pt-4 pb-4">
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Redirect from="*" to="/" />
</Switch>
</div>
</Router>
</div>
);
}
Global CSS Styles
The global stylesheet file contains CSS styles that are applied globally throughout the React application, it is imported in the main index.js file below.
a { cursor: pointer; }
.app-container {
min-height: 350px;
}
Main index.js file
The main index.js file bootstraps the React + Recoil app by rendering the App
component in the app
div element located in the main index html file.
The RecoilRoot
component is the context provider/store for Recoil state and is a required ancestor for any React components that access Recoil state. Wrapping it around the root App
component makes Recoil state global so it's accessible to all components in the React app.
The React.StrictMode
component doesn't render any elements in the UI, it runs in development mode to highlight potential issues/bugs in the React app. For more info see https://reactjs.org/docs/strict-mode.html.
Before starting the React app is started, the global CSS stylesheet is imported into the application and the fake backend API is enabled. To disable the fake backend simply remove or comment out the 2 lines below the comment // setup fake backend
.
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import './index.css';
import { App } from './App';
// setup fake backend
import { fakeBackend } from './_helpers';
fakeBackend();
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById('app')
);
dotenv
The dotenv file contains environment variables used in the example React app, the API URL is used in user actions to send HTTP requests to the API.
Environment variables set in the dotenv file that are prefixed with REACT_APP_
are accessible in the React app via process.env.<variable name>
(e.g. process.env.REACT_APP_API_URL
). For more info on using environment variables in React see https://create-react-app.dev/docs/adding-custom-environment-variables/
REACT_APP_API_URL=http://localhost:4000
jsconfig.json
The below configuration enables support for absolute imports to the application, so modules can be imported with absolute paths instead of relative paths (e.g. import { MyComponent } from '_components';
instead of import { MyComponent } from '../../../_components';
).
For more info on absolute imports in React see https://create-react-app.dev/docs/importing-a-component/#absolute-imports.
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
Package.json
The package.json file contains project configuration information including package dependencies that get installed when you run npm install
and scripts that are executed when you run npm start
or npm run build
etc. Full documentation is available at https://docs.npmjs.com/files/package.json.
{
"name": "react-recoil-jwt-authentication-example",
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^2.8.0",
"history": "^4.10.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.14.2",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"recoil": "^0.4.1",
"yup": "^0.32.9"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Need Some React Help?
Search fiverr for freelance React developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!