Implementing Silent Refresh on a React App using Typescript, setInterval, axios, and zustand
Tags: webdev, react, typescript, javascript
Published: 2022-07-14 12:30:46

## Introduction

In this post, I will show you how you can implement a silent refresh on React using Typescript, setInterval, axios, and zustand.

A year prior to writing this post, I attended an internal seminar hosted by the Young Software Engineers’ Society (an academic organization I’m affiliated with) wherein one of our alumnus thought us backend development from the theoretical level down to its implementation. On the last part, they briefly discussed authentication using json web tokens and how to secure your app using access and refresh tokens. To help us better understand it, they sent a link to [Hasura’s guide to securing JWT](https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/) . When I first read the article, I was so confused as to how we can implement silent refresh on React.

Almost a year later, I revisited the article because I was working on a new project, a desktop app that is, and we had to implement silent refresh on it. After several trial and errors, I finally got a running prototype that implements silent refresh on the background. And in this article I will share to you how I did it.

## Prerequisite

Once again, I won’t dive in too much on how silent refresh works. You can read up Hasura’s guide if you need a refresher.

To proceed, you must be at least familiar with the ff. topics / technologies

- React & React Hooks
- yarn (if you’re using npm, just install it)
- Typescript
- axios (or any http-fetching library)
- async / await
- Zustand
- JSON Web Tokens
- Git
- Have some familiarity with Node, Express, or backend development in general

## Setting-up Backend Server

To speed things up, I’ve prepared a backend server that you can clone for this mini tutorial. You may clone this repo by visiting this link or by running the ff. commands in your shell / command line

```bash
git clone https://github.com/dertrockx/example-auth-server.git
```

**SECURITY WARNING**

In Hasura’s guide, it was advised that the backend attaches the refreshToken to a secure HTTP cookie, so that the client does not have access to the refreshCookie using Javascript. However, for simplicity’s sake I didn’t do that, and instead I will let the client store the refreshToken as they please, as such, **this is an unsecure way of storing refreshTokens. Please be aware if you were to do this.**

After cloning the repository, run the ff. commands to install all dependencies and start the server

```bash
yarn # this is equivalent to 'npm install'
yarn dev # this is equivalent to 'npm run dev'
```

After running the commands above, your terminal / command line should look like this:
![Backend server running command line](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5bix9et79uun1oapfm8d.png)
 

The server provides two different endpoints that we will use for this mini tutorial. These are:

- `POST /auth/login` an endpoint that returns an access token, a refresh token, and a token_expiry - an integer value that tells you in milliseconds how long until the access token expires
- `GET /auth/refresh` an endpoint that returns a new set of token (access and refresh) and token_expiry - an integer value that tells you in milliseconds how long until the access token expires. This checks the header for a refresh token w/ the header name `Authorization` and w/ a value of `Bearer ${token-goes-here}`

Now that the backend is ready, let’s proceed with the frontend

## Creating the frontend application

First, we need create a blank react app that uses Typescript. For simplicity’s sake, we’ll use [create-react-app](https://create-react-app.dev/) with Typescript as the template. To do so, run the ff. commands

```bash
yarn create-react app --template typescript silent-refresh-app 
# the command above is equivalent to running npx create-react-app --template typescript silent-refresh-app
```

After initializing the project, we need to `cd` to the created directory. Just run `cd ./silent-refresh-app` and install other dependencies that we will use 

```bash
yarn add zustand axios # npm install zustand axios
```

- Zustand is a state management library that primarily encourages devs to use hooks and requires less boilerplate code than Redux
- Axios is an http client for the browser - it’s an alternative to the browser’s native Fetch API

### Create auth.service.ts

Once we have installed the dependencies, we can now send requests to the backend server. In order to do that, we need to [create a new instance of axios w/ custom config](https://axios-http.com/docs/instance). Simply create a new file called `axios.ts` under `src/lib` directory, w/ the ff. content:

```tsx
import axios from "axios";

// Creates a new instance of axios
// Just export this instance and use it like a normal axios object
// but this time, the root endpoint is already set
// So, when you do axios.get("/personnel") under the hood it actually calls axios.get("http://<your-path-to-backend-uri>")
const instance = axios.create({
	baseURL: "<your-path-to-backend-uri>" // can be http://localhost:8000
});

export default instance;
```

After doing so, we need to import this on a separate file that will call our backend api. We need to create a file called `auth.service.ts` under `src/services/` and add the ff. content

```tsx
import http from "../lib/http";
import axios, { AxiosError } from "axios";

// This interface is used to give structure to the response object. This was directly taken from the backend
export interface IHttpException {
	success: boolean;
	statusCode: number;
	error: string;
	code: string;
	message: string;
	details?: any;
}

// A custom error that you can throw to signifiy that the frontend should log out
export class ActionLogout extends Error {}
// service function to login

/**
* An function that attempts to log in a user.
* Accepts a username and a password, and returns the tokens and the token expiration or throws an error
*/
export async function login({
	username,
	password,
}: {
	username: string;
	password: string;
}): Promise<
	| {
			auth: string;
			refresh: string;
			tokenExpiry: number;
	  }
	| undefined
> {
	try {
		const credentials = {
			username: "admin",
			password: "password123",
		};
		// this is equal to http.post("http://<your-backend-uri>/auth/login", credentials);
		const res = await http.post("/auth/login", credentials);
		const {
			token: { auth, refresh },
			token_expiry,
		} = res.data;
		return { auth, refresh, tokenExpiry: token_expiry };
	} catch (err) {
		const error = err as Error | AxiosError;
		if (axios.isAxiosError(error)) {
			const data = error.response?.data as IHttpException;
			console.log(data.message);
			console.log(data.code);
			return;
		}
		console.error(error);
	}
}

/*
* An asynchronous function that refreshes the authenticated user's tokens.
* Returns a new set of tokens and its expiration time.
*/
export async function refreshTokens(token: string): Promise<
	| {
			auth: string;
			refresh: string;
			tokenExpiry: number;
	  }
	| undefined
> {
	try {
		// This is equivalent to http.get("http://<path-to-uri>/auth/refresh", { ... })
		const res = await http.get("/auth/refresh", {
			headers: {
				Authorization: `Bearer ${token}`,
			},
		});
		const {
			token: { auth, refresh },
			token_expiry,
		} = res.data;

		return { auth, refresh, tokenExpiry: token_expiry };
	} catch (err) {
		const error = err as Error | AxiosError;
		if (axios.isAxiosError(error)) {
			const data = error.response?.data as IHttpException;
			console.log(data.message);
			console.log(data.code);
			if (data.code === "token/expired") {
				throw new ActionLogout();
			}
		}
		console.error(error);
		return;
	}
}
```

After creating the services, we can then proceed with setting up our store

### Setting up Zustand Store

Zustand uses hooks, instead of the traditional duck-typing pattern of redux (yes, Redux now has [slices](https://redux-toolkit.js.org/api/createSlice), but for simplicity sake I used zustand because it is super lightweight and requires less boilerplate code to set-up compared to Redux). 

To create a new store, simply create a file named `auth.store.ts` under `src/store/` and add the ff. content (don’t worry, I’ll explain what they do)

```tsx
import create from "zustand";
import { devtools } from "zustand/middleware";

interface IAuthState {
	tokens: {
		auth: string;
		refresh: string;
	};
	count: number;
	tokenExpiry: number;
	authenticate: (
		tokens: {
			auth: string;
			refresh: string;
		},
		tokenExpiry: number
	) => void;
	logout: () => void;
	increment: () => void;
}

export const useAuth = create<IAuthState>()(
	devtools((set, get) => ({
		count: 0,
		tokens: {
			auth: "",
			// We will store the refresh token in localStorage. Again, this is an unsecure option, feel free to look for alternatives.
			refresh: localStorage.getItem("refreshToken") || "",
		},
		tokenExpiry: 0,
		increment: () => set({ count: get().count + 1 }),
		logout: () => {
			localStorage.setItem("refreshToken", "");
			set(() => ({
				tokens: {
					auth: "",
					refresh: "",
				},
				tokenExpiry: 0,
			}));
		},
		authenticate: (tokens, tokenExpiry) => {
			localStorage.setItem("refreshToken", tokens.refresh);
			set(() => ({
				tokens,
				tokenExpiry,
			}));
		},
	}))
);
```

To export the created store, create an `index.ts` file under `src/store/` that will export all of the content from `src/store/auth.ts`. Add the ff. content

```tsx
// src/store/index.ts
export * from "./auth.ts"
```

Why do we need this? So that when we want to use the auth store, all we have to do is import it from the folder, not the file itself

```tsx
// sample code when you want to import `useAuth`
// Assuming you're in a file under the 'src' directory
import { useAuth } from "./store"
```

### Edit App.tsx

Now that we have created our services and store, we then edit `App.tx` and use them inside it.

```tsx
import React, { useCallback, useRef } from "react";
import "./App.css";
// start of 1
import { useAuth } from "./store";
import { login, refreshTokens, ActionLogout } from "./services/auth.service";
import { useEffectOnce } from "./hooks";
// end of 1
function App() {
	// start of 2
	const {
		tokens: { refresh, auth },
		tokenExpiry,

		logout,
		authenticate,
	} = useAuth((state) => state);
	const intervalRef = useRef<NodeJS.Timer>();
	// end of 2

	// start of 3
	useEffectOnce(() => {
		if (refresh) {
			// try to renew tokens
			refreshTokens(refresh)
				.then((result) => {
					if (!result) return;
					const { auth, refresh, tokenExpiry } = result;
					authenticate({ auth, refresh }, tokenExpiry);
					intervalRef.current = setInterval(() => {
						console.log("called in useEffect()");
						sendRefreshToken();
					}, tokenExpiry);
				})
				.catch((err) => {
					if (err instanceof ActionLogout) {
						handleLogout();
					}
				});
		}
	});
	// end of 3
	

	// start of 4
	const handleLogout = useCallback(() => {
		logout();
		clearInterval(intervalRef.current);
		// eslint-disable-next-line
	}, [intervalRef]);

	const handleLogin = useCallback(async () => {
		const res = await login({ username: "admin", password: "password123" });
		if (!res) {
			return;
		}
		const { refresh: newRefresh, tokenExpiry, auth } = res;
		authenticate({ auth, refresh: newRefresh }, tokenExpiry);

		intervalRef.current = setInterval(() => {
			sendRefreshToken();
		}, tokenExpiry);

		// eslint-disable-next-line
	}, [refresh]);

	const sendRefreshToken = async () => {
		const refresh = localStorage.getItem("refreshToken")!;

		try {
			const result = await refreshTokens(refresh);
			if (!result) {
				return;
			}
			const { auth, refresh: newRefresh, tokenExpiry } = result;
			authenticate({ auth, refresh: newRefresh }, tokenExpiry);
		} catch (error) {
			if (error instanceof ActionLogout) {
				handleLogout();
			}
		}
	};

	// end of 4
	// start of part 5
	return (
		<div className="App">
			<p>
				{auth ? (
					<button onClick={() => handleLogout()}>Log out</button>
				) : (
					<button onClick={() => handleLogin()}>Login</button>
				)}
			</p>
			<p>
				Token expiry:{" "}
				{tokenExpiry !== 0 && new Date(Date.now() + tokenExpiry).toUTCString()}
			</p>
			<p>Auth token: {auth}</p>
			<p>Refresh token: {refresh}</p>
		</div>
	);
	// end of part 5
}

export default App;
```

I know what you’re thinking, *what the hell did I just copy-pasta-d into my code?* Don’t worry I’ll explain them, part-by-part

**Part 1: Imports**

First, we need to import three things - the service provider, the store, and a custom hook called `useEffectOnce`. What is this custom hook?

This custom hook let’s you run a useEffect only once. Since React 18, useEffect runs twice on development mode (insert link here). To prevent that, I’ll link a medium article that basically only runs useEffect once - on mount. 

Since this is a custom hook, you need to create this. Create a file called`src/hooks.ts` w/ the ff. content

```tsx
import { useRef, useState, useEffect } from "react";

export const useEffectOnce = (effect: () => void | (() => void)) => {
	const destroyFunc = useRef<void | (() => void)>();
	const effectCalled = useRef(false);
	const renderAfterCalled = useRef(false);
	const [, setVal] = useState<number>(0);

	if (effectCalled.current) {
		renderAfterCalled.current = true;
	}

	useEffect(() => {
		// only execute the effect first time around
		if (!effectCalled.current) {
			destroyFunc.current = effect();
			effectCalled.current = true;
		}

		// this forces one render after the effect is run
		setVal((val) => val + 1);

		return () => {
			// if the comp didn't render since the useEffect was called,
			// we know it's the dummy React cycle
			if (!renderAfterCalled.current) {
				return;
			}
			if (destroyFunc.current) {
				destroyFunc.current();
			}
		};
		// eslint-disable-next-line
	}, []);
};
```

To save time, I’ll just attach a link to the original medium article that further explains this.

**Part 2: Getting state and Ref**

In part of the `App.tx` file, you can see that we extract the state values and actions that are inside `auth.ts` Since we need to renew the tokens every *X* seconds (where *X* is any integer > 0 in milliseconds)  and send a request to the backend, we are going to user `setInterval` and store its intervalId without triggering a re-render. To do so, we have to use `useRef` and pass a type of **`NodeJS.Timer`** to let Typescript do its magic by giving suggestions when we write code.

```tsx
const {
	tokens: { refresh, auth },
	tokenExpiry,
	logout,
	authenticate,
} = useAuth((state) => state);
// we pass NodeJS.Timer to useRef as its value's type

const intervalRef = useRef<NodeJS.Timer>();
```

**Part 3: Using the custom hook `useEffectOnce`**

[Starting React 18, a component is mounted, unmounted, then mounted again](https://www.techiediaries.com/react-18-useeffect/). This makes useEffect hooks that have no dependencies run twice - that is why we had to use a custom useEffect hook that will only run once (I forgot where I originally found the custom hook - I’ll leave something in the comment section or I’ll update this once I find it).

The function passed inside the `useEffectOnce` is just like any normal function passed to a `useEffect` hook. On initial page load, we want to fetch a new set of tokens (access and refresh) and re-fetch another set of tokens every *X* seconds (*tokenExpiry*).  Here, we call the function `refreshTokens()` from the `auth.service.ts` where we pass in a refresh token. It returns a promise that resolves a new auth (or access) token, refresh token, and a *tokenExpiry*. We will then update the store, and start the silent refresh process.

```tsx
useEffectOnce(() => {
	if (refresh) {
		// try to renew tokens
		refreshTokens(refresh)
			.then((result) => {
				if (!result) return;
				const { auth, refresh, tokenExpiry } = result;
				// Update the store
				authenticate({ auth, refresh }, tokenExpiry);
				// start the silent refresh
				intervalRef.current = setInterval(() => {
					sendRefreshToken();
				}, tokenExpiry);
			})
			.catch((err) => {
				// if the service fails and throws an ActionLogout, then the token has expired and in the frontend we should logout the user
				if (err instanceof ActionLogout) {
					handleLogout();
				}
			});
	}
});
```

**Part 4: The methods that handle login, logout, and sendRefreshToken**

Now that we have set up the background refresh on initial load, I then explain the functions that are called when the user clicks on the button to login / logout and sending of refresh token.

But first, I know what you’re thinking - *But Ian, why are you using useCallback, and what the hell is it? -* `useCallback` is a hook that React provides out-of-the-box that accepts two paremeters - a function, and a list of dependencies. The function passed is cached and is only rebuilt when the dependencies change. 

Why does this exist? Because [when a component re-renders, functions inside of it are rebuilt too](https://stackoverflow.com/questions/41369296/react-functions-inside-render) and it hits the performance of your app (you can further google it).  For small-scale apps, this is not much of an issue but for big apps, this is very crucial. So, developers need to find a way to cache functions and only rebuild them when necessary - hence `useCallback` was created.

```tsx
const handleLogout = useCallback(() => {
	logout();
	clearInterval(intervalRef.current);
	// eslint-disable-next-line
}, [intervalRef]);

const handleLogin = useCallback(async () => {
	const res = await login({ username: "admin", password: "password123" });
	if (!res) {
		return;
	}
	const { refresh: newRefresh, tokenExpiry, auth } = res;
	authenticate({ auth, refresh: newRefresh }, tokenExpiry);

	intervalRef.current = setInterval(() => {
		sendRefreshToken();
	}, tokenExpiry);

	// eslint-disable-next-line
}, [refresh]);

const sendRefreshToken = async () => {
	const refresh = localStorage.getItem("refreshToken")!;

	try {
		const result = await refreshTokens(refresh);
		if (!result) {
			return;
		}
		const { auth, refresh: newRefresh, tokenExpiry } = result;
		authenticate({ auth, refresh: newRefresh }, tokenExpiry);
	} catch (error) {
		if (error instanceof ActionLogout) {
			handleLogout();
		}
	}
};
```

The first function, `handleLogout()` is a memoized function that runs `logout()` from the `useAuth()` hook  (which clears the store) and cleans up the function that runs in the background (the silent-refresh part) identified by `intervalRef.`

The second function, `handleLogin()`, is a memoized function that runs when the user presses the `Login` button. Internally, it calls `login()` that then tries to send user credentials to the backend server. If it succeeds, then it returns a new set of tokens (auth and refresh) and a tokenExpiry. We then use this tokenExpiry to send a request to the backend server to refresh (*see what I did there?*) the tokens and refresh it again - creatinga silent refresh feature. 

The last function, `sendRefreshToken()` is a function that is called by the `handleLogin()` function that refreshes the token. As you can see here, we access the refresh token by directly accessing it from the localStorage instead of via the store. Why? TBH I’m not really certain why - somehow a Zustand store state does not persiste when it is referenced inside `setInterval`.

**Part 5: Rendering UI**

After defining all of the functions and logic, we then render JSX content that uses the login / logout functions depending on the state of the store

```tsx
return (
	<div className="App">
		<p>
			{auth ? (
				<button onClick={() => handleLogout()}>Log out</button>
			) : (
				<button onClick={() => handleLogin()}>Login</button>
			)}
		</p>
		<p>
			Token expiry:{" "}
			{tokenExpiry !== 0 && new Date(Date.now() + tokenExpiry).toUTCString()}
		</p>
		<p>Auth token: {auth}</p>
		<p>Refresh token: {refresh}</p>
	</div>
);
```

Once you are done with everything, save it, and run the dev server by running the ff. command to your shell / command line
Once you are done with everything, save it, and run the dev server by running the ff. command to your shell / command line

```bash
yarn start # this is equivalent to npm start
```

If it runs, it should automatically open your browser at [http://localhost:3000](http://localhost:3000). If it doesn’t, you can just open it for yourself. You should see something like this.

![App running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qwn1yqyrdlxyncn26dgu.gif)

By default, I set the expiration time of the auth token to 5 seconds and for the refresh token to 10 seconds. As you can see, the tokens are refreshed every 5 seconds. Also, if you try to refresh the page, the tokens are still refreshed every 5 seconds since it runs the silent-refresh on initial page load.

To test if the refresh token actually expires, you can close the tab, wait for more than 10 seconds, and then revisit the same [site](http://localhost:3000). It should not run the silent-refresh on the background and instead automatically logout since the refresh token has expired. Also, you should see something like this *(note: you have to open your dev tools to see the error)*

![Refresh token expired](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jgw31afq2uhe4yzt311o.png)

To re-run the silent refresh, just click on `login.`

## Conclusion

Implementing silent refresh is tricky - you have to use setInterval to run a function periodically and you must ensure that this function is cleared if not used. 

Silent refresh is a nice security feature, but this article only scrapes the tip of the iceberg - for further readings, I highly recommended reading [hasura’s official guide](https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/).

Here’s a copy of the frontend repository → [https://github.com/dertrockx/react-silent-refresh/](https://github.com/dertrockx/react-silent-refresh/)

Here’s for the backend → [https://github.com/dertrockx/example-auth-server](https://github.com/dertrockx/example-auth-server)