Building a secure and seamless authentication system is essential to protect user data and provide personalized experiences. In this tutorial, we will delve into the process of creating an authentication context in your project using React.
By implementing an auth context, you can centralize and manage authentication-related states throughout your application. This powerful approach simplifies the sharing of user authentication data across components, eliminating the need for prop drilling or complex state management solutions.
Throughout this guide, we will explore the step-by-step process of setting up an auth context, integrating authentication functionality, and consuming the context in various components. By the end, you will have a solid understanding of how to create a flexible and scalable authentication system in your React projects.
Whether you're working on a personal project, a small application, or a large-scale enterprise application, mastering the art of creating an auth context will undoubtedly enhance your front-end development skills. So, let's dive in and unlock the secrets of building a robust authentication system using React's auth context!
In order to implement the auth context into our React project, first we need to build the AuthContext.tsx file. Let’s create a User interface that will return from the backend on our calls.
interface User {
status: string;
_id: string;
email: string;
nameSurname: string;
confirmationCode: string;
__v: number;
tempSecret: string;
tempToken: string;
secret: string;
token: string;
lastAccessed: string;
}
In order for Typescript to not throw any error on our code, we need to create an initial user that will look like this.
const initialUser: User = {
status: '',
followers: [],
_id: '',
email: '',
nameSurname: '',
confirmationCode: '',
__v: 7,
tempSecret: '',
tempToken: '',
etsyId: '',
secret: '',
token: '',
lastAccessed: '',
};
This is not mandatory and there are other ways of doing it but I prefer to do it this way. Now that we know what our User object looks like, we can start working on our AuthContext logic. Apart from the user object, we’ll return some functions in the AuthContext such as loginUser, logoutUser, registerUser, changePassword, and forgetPassword. These will be empty functions with userData as props. No need to worry about these empty functions because in the next step, we’ll introduce the actual functions.
export const AuthContext = createContext({
user: initialUser,
loginUser: (userData: User) => {},
logoutUser: () => {},
registerUser: (userData: User) => {},
changePassword: (userData: User) => {},
forgetPassword: (userData: User) => {},
});
After we defined our AuthContext, now we need to create an AuthProvider which will wrap up our application. AuthProvider will be the function where all the backend calls are made, and the state of the user is preserved. The AuthProvider will be in the shape of;
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const navigate = useNavigate();
const storedData = localStorage.getItem('authData');
const [user, setUser] = useState(storedData ? JSON.parse(storedData) : undefined);
const loginUser = (userData: User) => {
axios
.post(`${LOGIN_USER_ENDPOINT}`, userData)
.then((res) => {
setUser(decoded);
localStorage.setItem('authData', JSON.stringify(decoded));
navigate('/dashboard');
})
.catch((err) => {
// Handle Error
});
};
const logoutUser = () => {
setUser(null);
localStorage.removeItem('authData');
navigate('/');
};
const registerUser = (userData: User) => {
axios
.post(`${CREATE_USER_ENDPOINT}`, userData)
.then((res) => {
setUser(res.data);
localStorage.setItem('authData', JSON.stringify(data));
navigate('/register-success');
})
.catch((err) => {
// Handle Error
});
};
const changePassword = (payload: User) => {
axios
.post(`${CHANGE_PASSWORD_ENDPOINT}`, payload)
.then(() =>
navigate('/dashboard');
})
.catch((err) => {
// Handle Error
});
};
const forgetPassword = async (payload: User) => {
axios
.post(`${FORGET_PASSWORD_ENDPOINT}`, payload)
.then(() => {
// Handle Success
})
.catch((err) => {
// Handle Error
});
};
const authContextValue = {
user,
loginUser,
logoutUser,
registerUser,
changePassword,
forgetPassword,
};
return <AuthContext.Provider value={authContextValue}>{children}</AuthContext.Provider>;
};
Let’s break down this code to understand what is happening in each step;
- The
AuthProvider
the component is exported as a named export usingexport const
. It takes a single propchildren
of typeReactNode
, which represents the child components that will have access to the authentication context. - The
navigate
function from theuseNavigate
hook is assigned to thenavigate
variable. This hook provides the ability to navigate to different routes programmatically. - The
storedData
the variable is assigned the value retrieved from thelocalStorage
the key'authData'
. It stores the serialized user authentication data if it exists. - The
user
state variable is declared using theuseState
hook. It is initialized with the parsed value ofstoredData
if it exists, orundefined
otherwise. This state variable represents the currently authenticated user. - The
loginUser
function takes auserData
object as a parameter, which represents the user credentials. It makes an asynchronous HTTP POST request usingaxios
to a login endpoint (LOGIN_USER_ENDPOINT
) with the provided user data. If the request is successful (then
block), it sets theuser
state with the decoded response data stores the serialized user data in, and navigates the user to the '/dashboard' route. If the request fails (catch
block), you can add an error handling code to handle the error appropriately. - The
logoutUser
the function is responsible for logging out the user. It sets theuser
state tonull
, removes the serialized user data from, and navigates the user to the '/' route. - The
registerUser
function is similar tologinUser
, but it sends a POST request to a register endpoint (CREATE_USER_ENDPOINT
) with the provided user data. On success, it sets theuser
state with the response data, stores the serialized user data inlocalStorage
, and navigates the user to the '/register-success' route. You can add error handling code in case the request fails. - The
changePassword
function is responsible for changing the user's password. It sends a POST request to a change password endpoint (CHANGE_PASSWORD_ENDPOINT
) with the provided new password. On success, it navigates the user to the '/dashboard' route. You can handle errors appropriately in thecatch
block. - The
forgetPassword
function handles the process of requesting a password reset. It sends a POST request to a forget password endpoint (FORGET_PASSWORD_ENDPOINT
) with the provided payload and it’ll send you a link to change your password. You can handle success and error scenarios accordingly. - An
authContextValue
object is created, which contains theuser
state, along with the defined authentication functions (loginUser
,logoutUser
,registerUser
,changePassword
, andforgetPassword
). - The
AuthProvider
component returns theAuthContext.Provider
component, which wraps thechildren
prop. It provides theauthContextValue
as the value of the authentication context, allowing the child components to access the user state and authentication functions.
Now the last part for this context to work is to wrap the whole application with it. in your index.ts
file, we can do that like this.
import App from './App';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from 'context/auth/AuthContext';
const container = document.getElementById('root');
const root = createRoot(container as HTMLDivElement);
root.render(
<StrictMode>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</StrictMode>
);
Now, we can consume our auth context anywhere in our project. To do that, I would like to create a custom hook for it. Let’s call it useAuth, this way, our code will look much more cleaner and compact.
import { useContext } from 'react';
import { AuthContext } from 'context/auth/AuthContext';
export const useAuth = () => {
const authContext = useContext(AuthContext);
if (!authContext) {
throw new Error('useAuth must be used within an AuthProvider');
}
return authContext;
};
As you can see, it is not the end of the world and doesn’t change much but I adore how the useAuth looks on the code. Also, I would like to create some other custom hooks related to auth context so that we don’t need to write the same code each time.
export const useName = () => {
const { user } = useAuth();
const [name, surname] = user?.nameSurname?.split(' ');
const nameInitial = name.charAt(0);
const surnameInitial = surname.charAt(0);
const nameSurname = user?.nameSurname?.split(' ');
const userName = nameInitial + surnameInitial;
const fullName = nameSurname?.length >= 2 ? nameSurname?.[0] + ' ' + nameSurname?.[1] : nameSurname?.[0];
return { userName, nameSurname, fullName };
};
export const useUserMail = () => {
const { user } = useAuth();
return { email: user?.email };
};
Let’s see what is happening here;
useName
is a custom hook that extracts the user's name and surname from the authentication context. It splits thenameSurname
string into separate variables, captures the initials of the name and surname, and constructs auserName
by concatenating the initials. It also creates afullName
by combining the first and second elements of thenameSurname
array, or using the first element if there is no second element. The hook returns an object containinguserName
,nameSurname
, andfullName
.useUserMail
is another custom hook that retrieves the user's email from the authentication context. It returns an object with a single property,email
, which is set to the value ofuser?.email
.
By using these hooks in our code, we can easily access and utilize the user's name, email, and related information from the authentication context without explicitly accessing the context or user object every time.
Now let’s see how we can use all these custom hooks and functions in our context;
const LoginForm = () => {
const { loginUser } = useAuth();
const onSubmit = (values: LoginFormValues) => {
loginUser(values);
};
return (
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<label htmlFor="email">
<h3>Email Address</h3>
<Field name="email">
{({ input, meta }) => (
<div>
<input {...input} id="email" type="email" placeholder="Email Address" />
{meta.error && meta.touched && <div className="error-message">{meta.error}</div>}
</div>
)}
</Field>
</label>
<label htmlFor="password">
<h3>Password</h3>
<Field name="password">
{({ input }) => (
<div>
<input {...input} id="password" type="password" placeholder="Password" />
</div>
)}
</Field>
</label>
<Button type="submit" variant="primary">
Login
</Button>
</form>
)}
/>
);
};
As you can see, by only calling the loginUser function and connecting it to form submit with the necessary information we can use AuthContext in our project. The same way as the loginUser, we can also get user and fullName from our hooks like this to use in our code,
const { user } = useAuth();
const { fullName } = useName();
Additional Considerations
Creating an authentication context is a powerful technique for managing user authentication in your React projects. However, there are a few additional considerations you may want to keep in mind:
- Security: While this tutorial focuses on setting up the authentication context, it's crucial to prioritize security. Implement proper security measures, such as secure token storage, secure communication channels (HTTPS), and appropriate server-side validation and authorization checks.
- Error Handling: The code examples provided in this tutorial demonstrate the basic flow of authentication actions. However, it's essential to implement robust error handling mechanisms to handle various scenarios, such as network errors, server-side validation errors, and authentication failures. Ensure that you handle errors gracefully and provide meaningful feedback to your users.
- Remember Me Functionality: If your application requires a "Remember Me" functionality to keep users logged in across sessions, you'll need to consider additional implementation steps. This may involve storing long-lived tokens securely and providing mechanisms to manage session expiration and revocation.
- Two-Factor Authentication (2FA): If you want to enhance the security of your authentication system, consider integrating two-factor authentication. This can involve using SMS verification, email codes, or authenticator apps to add an extra layer of security for user authentication.
Conclusion
In this blog post, we explored the process of creating an authentication context in a React project. We learned how to set up the auth context, integrate authentication functionality, and consume the context in various components. By using the auth context, we can efficiently manage user authentication state, share data between components, and build secure and user-friendly authentication systems.
Remember to adapt the code examples to your specific project requirements and consider additional security measures and error handling strategies. Creating an authentication context empowers you to build robust and scalable authentication systems, ensuring a smooth and secure user experience.
With the knowledge gained from this tutorial, you are well-equipped to implement an auth context in your React projects. Embrace the power of React's auth context and elevate the security and functionality of your applications. Happy coding!