Tired of writing the same auth code over and over again every time you start a new project? Let's build a simple authentication system using React Context. This way we can easily reuse it in all our projects, completely independat of the state management library we use.
Why React Context and not Redux, recoil, zustand...
React Context is a very powerful tool, but it's not the only tool in the shed. There are a lot of other state management libraries out there that can do the same thing. So why use React Context? Well, it's simple really. React Context is built into React, it's a native feature. This means that we don't need to install any third party libraries to use it. This also means that we can use it in any React project, even if we're not using any other state management library.
In fact handling auth is a damn near perfect candidate for react context, because by it's very nature it belongs in a global application state and we don't expect it to change very often.
What we're going to build
We're going to build a very simple authentication system with react context and react hooks that will allow us to:
- Store the user's information in the context and local storage
- Check if the user is logged in
- clear the user's information from the context and local storage
- retrieve the user's information from the context and local storage
WE WILL NOT build a fully fledged authentication system with login, logout, registration, etc. We will only build the frontend part, meanig the part that will allow our frontend to know if the user is logged in or not and react accordingly, meaning we will not:
- send the user's credentials to the backend
- verify the user's credentials
- store the user's credentials in the backend
I'll assume you're already familiar with React and TypeScript, I'll also assume you've already have a react or next.js app set up. If you don't, use create-react-app, create-next-app or even t3-app to get started.
Step 1: React Hooks
Let's start off by creating a couple of custom hooks that we'll require for out auth system, first create a directory called hooks
in your src
directory then create the following files in there:
useAuth.ts
useUser.ts
useLocalStorage.ts
useLocalStorage.ts
This hook will allow us to easily store and retrieve data from localStorage. It's a very simple hook, but it's a good idea to keep it in a separate file so we can reuse it in other parts of our application.
import { useState } from "react";
export const useLocalStorage = () => {
const [value, setValue] = useState<string | null>(null);
const setItem = (key: string, value: string) => {
localStorage.setItem(key, value);
setValue(value);
};
const getItem = (key: string) => {
const value = localStorage.getItem(key);
setValue(value);
return value;
};
const removeItem = (key: string) => {
localStorage.removeItem(key);
setValue(null);
};
return { value, setItem, getItem, removeItem };
};
useUser.ts
This hook will store the user in our context and localStorage.
import { useContext } from "react";
import { AuthContext } from "../context/AuthContext";
import { useLocalStorage } from "./useLocalStorage";
// NOTE: optimally move this into a separate file
export interface User {
id: string;
name: string;
email: string;
authToken?: string;
}
export const useUser = () => {
const { user, setUser } = useContext(AuthContext);
const { setItem } = useLocalStorage();
const addUser = (user: User) => {
setUser(user);
setItem("user", JSON.stringify(user));
};
const removeUser = () => {
setUser(null);
setItem("user", "");
};
return { user, addUser, removeUser, setUser };
};
useAuth.ts
This is the hook where it all comes together. This react hook will be responsible for checking if the user is logged in or not, and if they are, it will return the user object. If the user is not logged in, it will return null.
We'll also expose a login
and logout
function that we can use to log in and log out the user.
import { useEffect } from "react";
import { useUser, User } from "./useUser";
import { useLocalStorage } from "./useLocalStorage";
export const useAuth = () => {
// we can re export the user methods or object from this hook
const { user, addUser, removeUser, setUser } = useUser();
const { getItem } = useLocalStorage();
useEffect(() => {
const user = getItem("user");
if (user) {
addUser(JSON.parse(user));
}
}, [addUser, getItem]);
const login = (user: User) => {
addUser(user);
};
const logout = () => {
removeUser();
};
return { user, login, logout, setUser };
};
Step 2: React Context
Now that we have our hooks ready, let's create our context. Create a directory called context
in your src
directory and create a file called AuthContext.tsx
in it.
import { createContext } from "react";
import { User } from "../hooks/useUser";
interface AuthContext {
user: User | null;
setUser: (user: User | null) => void;
}
export const AuthContext = createContext<AuthContext>({
user: null,
setUser: () => {},
});
Step 3: Wrap your app in the context provider
Now that we have our context ready, let's wrap our app in it. Open your App.tsx
file and wrap your app in the context provider.
import { AuthContext } from "../context/AuthContext";
import { useAuth } from "../hooks/useAuth";
const App = () => {
const { user, login, logout, setUser } = useAuth();
return (
<AuthContext.Provider value={{ user, setUser }}>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
</AuthContext.Provider>
);
};
export default App;
Step 4: Create a login page
Now that we have our context ready, let's create a login page. Create a directory called pages
in your src
directory and create a file called login.tsx
in it.
import { useAuth } from '../hooks/useAuth';
const Login = () => {
const { login } = useAuth();
const handleLogin = () => {
login({
id: '1',
name: 'John Doe',
email: 'john.doe@email.com',
});
};
return (
<div>
<h1>Login</h1>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;
Step 5: Create a logout button
Awesome now we have a login page all there's missing is a way to log out, let's create a logout button. Create a file called LogoutButton.tsx
in your src
directory.
import { useAuth } from '../hooks/useAuth';
const LogoutButton = () => {
const { logout } = useAuth();
return <button onClick={logout}>Logout</button>;
};
export default LogoutButton;
Further steps
Now that we have our helpful hooks and context ready all we need to do is use them in our application, for example we can call our useAuth
hook in our App.tsx
to check if the user is logged in or not, if not we can redirect the user to the login page. We can also conditionally render the logout button based on whether the user is logged in or not.
All of this is completely independant from any and all state management libraries.
A note on localStorage
I've used localStorage in this example, but you can use any other storage mechanism you want. For example, you can use cookies The only thing that matters is that you store the user object in a way that you can retrieve it later.
Additionally you should also probably encrypt the user object before storing it in localStorage, but that's out of the scope of this article. Also you should probably allow the user select if they wish to persist their login after leaving the page or not.
IMPORTANT This is not a secure way to store user data, it's just a simple example to show how you can use react hooks to create a simple authentication system. You should optimally use a secure authentication system like JWT or OAuth, but that's out of the scope of this article.
Conclusion
In this article we've learned how to create a simple authentication system using react hooks and context. We've also learned how to persist the user object in localStorage so that the user doesn't have to log in every time they visit the page.