Sharing Data Across your React App with React Context
Jake Lundberg
Posted on February 12, 2023
It's very common to need to share data across different parts of a React application, and React Context can be a very useful method for doing this when the data being shared doesn't change very often.
In this post, we'll build a small React application that uses React Context to share the user's data with it's different components.
I highly encourage you to follow along on your own, but if you get stuck, or just want to see the code without working through each step, you can refer to the GitHub repo I am sharing along with it.
Before Continuing
In this post, I assume that you're already familiar with the basics of React Context, and the problem it seeks to address. So we will only be focussing on it's implementation within an application. If you're not yet familiar with React Context and the problem it solves, freeCodeCamp has a great article I would highly recommend you check out!
I would also like to stress that React Context is one of many possible solutions, and your specific use case will determine if it's the RIGHT solution for you. So please do not take this post as a "one size fits all" answer.
With all that out of the way, let's dive in!
Setup
To start, let's create our project. I'll be using Vite for this, and will use their react-ts
template preset to create a new React project using Typescript. (if you don't want to use Typescript, you can use Vite's react
template preset instead. Just know that your code will vary slightly from mine throughout this post.) (see Vite's Getting Started page for more information.)
Create the project by running the following command in your terminal:
npm create vite@latest xample-react-context -- --template react-ts
then run to install the project's initial dependencies:
cd xample-react-context && npm install
Now that we have a fresh project, let's add a couple of dependencies we'll use later.
npm i react-router react-router-dom
Let's also get rid of some of the boilerplate code that was generated by Vite.
We won't need the default styling so let's remove the /src/App.css
file:
rm src/App.css
And replace all the code inside of /src/index.css
with:
/src/index.css
* {
margin: 0;
padding: 0;
color: #f1f1f1;
box-sizing: border-box;
}
body {
min-width: 100vw;
min-height: 100vh;
background: #1f1f1f;
}
Next, replace all the code inside of /src/App.ts
with the following:
function App() {
return (
<div className="app">
Hello World
</div>
)
}
export default App
Now if you run npm run dev
you should see the following:
Great, we now have a blank canvas to begin our work!
Refer to branch 001-setup if you're having any issues.
Routes
Next, let's set up a few screens for ourselves to work with. For now, let's start with a home screen, a sign in screen, a profile screen, and a not found screen.
Create a new directory within /src
called screens
. This is where we'll store the root component for each of our screens.
When I create components, I like to have a directory for each one, and inside that directory are all the files that pertain to the specific component. I find this very useful, especially in larger projects where we might have many files for every component, such as a file for styles (if you separate them from your .[j|t]sx
files), .stories.tsx
to use with Storybook, and a .cy.ts
for Cypress Component testing. We aren't going to get into all those files in this post, but I am going to use the same structure here, so I wanted to explain why I structure my apps this way.
Let's begin with a base level Screen
component so all our pages will have the same structure.
Create a new directory in /src/screens
called Screen
, and add an index.tsx file inside it. Then add the following code:
/src/screens/Screen/index.tsx
import { FC, ReactNode } from 'react';
interface IProps {
children: ReactNode;
className?: string;
}
export const Screen: FC<IProps> = ({
children,
}) => {
return (
<div className='screen'>
{ children }
</div>
);
};
This component isn't doing much right now, but we'll change that a little later.
Now, let's create each of our individual screens. I'll start with the Sign In screen. So inside /src/screens
create a new directory called SignIn
, add an index.tsx
file inside it, and add the following code to the file:
/src/screens/SignIn/index.tsx
import { FC } from 'react';
import { Screen } from '../Screen';
export const SignIn: FC = () => {
return (
<Screen className='sign-in-screen'>
Sign In Screen
</Screen>
);
}
Now the Home
screen.
/src/screens/Home/index.tsx
import { FC } from 'react';
import { Screen } from '../Screen';
export const Home: FC = () => {
return (
<Screen className='home-screen'>
Home Screen
</Screen>
);
}
Then the Profile
screen.
/src/screens/Home/index.tsx
import { FC } from 'react';
import { Screen } from '../Screen';
export const Profile: FC = () => {
return (
<Screen className='profile-screen'>
Profile Screen
</Screen>
);
}
And lastly the NotFound
screen.
/src/screens/NotFound/index.tsx
import { FC } from 'react';
import { Screen } from '../Screen';
export const NotFound: FC = () => {
return (
<Screen className='not-found-screen'>
Not Found Screen
</Screen>
);
}
Now that we have our pages, let's now create a router so we can navigate between them.
Create a new directory in /src
called routers
. This is where we'll store all the routers for our app. For this post, we will only have 1, but it's still good practice to keep things organized incase this app ever grows!
Inside /src/routers
create a new file called main.tsx
. This will house the code for the primary router of our application. Inside this file, and add the following code:
/src/routers/main.tsx
import { Route, Routes } from 'react-router';
import { Home } from '../screens/Home';
import { NotFound } from '../screens/NotFound';
import { Profile } from '../screens/Profile';
import { SignIn } from '../screens/SignIn';
export const MainRouter = () => {
return (
<Routes>
<Route path='/' element={ <Home /> } />
<Route path='/profile' element={ <Profile /> } />
<Route path='/signin' element={ <SignIn /> } />
<Route path='*' element={ <NotFound /> } />
</Routes>
);
};
Now that we have our routes setup, let's update /src/App.tsx
by removing our initial Hello World content, and replacing it with the following:
/src/App.tsx
function App() {
return (
<BrowserRouter>
<MainRouter />
</BrowserRouter>
)
}
Notice that we wrap our MainRouter
with React Router's BrowserRouter
in the App component, and not inside our MainRouter
component. This is a personal preference of mine to keep my router components clean. As you will see later, we are going to add more components to this file, and I prefer my router components ONLY contain router code.
If you run npm run dev
, you should now be able to go to the following routes and see our screens in the browser:
Note, your port may be different if you are running other Vite apps at the same time. To confirm what port you need, look in your terminal when you execute npm run dev
. Vite will print out the url for you to use.
- Home -
http://localhost:5173/
- SignIn -
http://localhost:5173/signin
- Profile -
http://localhost:5173/profile
- NotFound -
http://localhost:5173/any-other-path
Our app structure should now look like this:
Refer to branch 002-routers if you're having any issues.
Navigation
Alright, we have our pages and routes set up, but it's a little annoying that we have to manually type our routes into the browser's address bar to view them. Let's add a navigation bar so we can more easily switch between pages.
We're going to add this navigation bar to our Screen
component so it will automatically be displayed on all our pages.
To start, let's create a new directory inside /src
called components
. This is where we will store all our shared components for the app.
Inside /src/components
, create a directory called MainNav
, add an index.tsx
, and add the following code.
/src/components/MainNav/index.tsx
import { FC } from "react";
import { Link } from "react-router-dom";
import "./styles.css";
interface IProps {
className?: string;
}
export const MainNav: FC<IProps> = ({ className }) => {
return (
<nav className={`main-nav ${className}`}>
<Link to='/' className='logo'>
Xample
</Link>
<Link to='/signin'>
Sign In
</Link>
</nav>
);
};
Let's also add some styling to make our nav more usable:
/src/components/MainNav/styles.css
.main-nav {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: space-between;
align-items: center;
width: 100vw;
height: 5rem;
padding: 0 1rem;
background-color: #136f93;
}
.main-nav a {
font-family: Arial, Helvetica, sans-serif;
text-decoration: none;
}
.main-nav a:hover,
.main-nav a:focus-within {
text-decoration: underline;
}
.main-nav a:hover {
cursor: pointer;
}
.main-nav .logo {
font-size: 3rem;
}
And now let's update our Screen
component so all our pages display this new nav.
/src/screens/Screen/index.tsx
import { FC, ReactNode } from 'react';
import { MainNav } from '../../components/MainNav';
import './styles.css';
interface IProps {
children: ReactNode;
className?: string;
}
export const Screen: FC<IProps> = ({
children,
}) => {
return (
<div className='screen'>
<MainNav />
<div className='screen-content'>
{ children }
</div>
</div>
);
};
/src/screens/Screen/styles.css
.screen-content {
margin-top: 5rem;
padding: 1rem;
}
There we go, now if you run npm run dev
you should see:
If you click the "Sign In" link, you should be redirected to the SignIn
screen, and if you click the "Xample" logo, you should return to the Home
screen.
We will add a link to the Profile
screen once we are able to identify if the user is signed in.
Refer to branch 003-navigation if you are having any issues.
The UserSession Context
Okay, now it's finally time to add our React Context. But before we do, let's first consider the functionality we'll need.
- When the user is NOT signed in, they should see a "Sign In" link on the right side of the nav bar.
- When the user IS signed in, they should see their avatar along with a "Sign Out" link on the right side of the nav bar.
- when the Avatar is clicked, the user should be redirected to
/profile
- when the "Sign Out" link is clicked, the user should be signed out, and redirected to
/signin
.
- when the Avatar is clicked, the user should be redirected to
- If the user is NOT signed in and they land on the
/profile
route, they should be redirected to/signin
. - If the user IS signed in and they land on the
/profile
route, they should see their personal information and be able to edit it.- if the user edits the information and clicks "Save" their information should be updated throughout the app.
- If the user is NOT signed in and they land on the
/signin
route, they should see theSignIn
screen where they can enter their username and password and sign in. - If the user IS signed in and they land on the
/signin
route, they should be redirected to/
.
From this we can see that we will need to,
- have a way of easily identifying if the user is signed in or not
- allow the user to submit their username and password to sign in
- get the user's information
- update the user's information
- allow the user to sign out
Alright, now that we know our requirements, we're ready to dive in!
We'll start by creating a new directory inside of /src
called contexts
. This is where we will store any contexts we create for our app. Inside this new directory, let's create a new file called user-session.tsx
.
I name this file user-session
because it will manage all data and functionality around a user's session, but feel free to name yours whatever you like!
Inside this file, add the following:
/src/contexts/user-session.tsx
import { createContext, FC, ReactNode, useContext, useMemo, useRef, useState } from 'react';
interface IUserSession {
isSignedIn: boolean;
}
interface IProps {
children: ReactNode;
}
const UserSessionContext = createContext<IUserSession | null>(null);
export const useUserSession = () => useContext(UserSessionContext);
export const UserSessionProvider: FC<IProps> = ({ children }) => {
const userSession: IUserSession = useMemo(() => ({
isSignedIn: false,
}), []);
return (
<UserSessionContext.Provider value={ userSession }>
{ children }
</UserSessionContext.Provider>
);
};
Let's step through this code so we know everything that's going on.
On Line 1 we're importing a few things from React. We'll touch on each of them in the next few steps.
Lines 3-5 are defining the structure our context will have using a Typescript interface. For now, our context will just have 1 property, but we will add more later.
Lines 7-9 are defining the props that our Functional Component below will take.
On Line 11 we're creating a new React Context using createContext
. Notice we are using our interface from Line 3 here to specify the structure this context will have.
On Line 13 we're creating our own custom hook which we will use to access our context throughout our app. This is just a small convenience I like to do in my apps rather than calling useContext(UserSessionContext)
everywhere. There is nothing wrong with using useContext(UserSessionContext)
directly instead if that is how you prefer to write your code.
Lines 15-25 define a regular React Functional Component named UserSessionProvider
, which we'll use to house all our user session specific logic.
Inside UserSessionProvider
, on lines 16-18, we're creating a regular javascript object that's going to contain the properties the rest of our app is going to access. Notice I've wrapped the object in React's useMemo
hook so we aren't creating a new object every time this component rerenders.
Finally, on lines 20-24, we're returning the context provider with our userSession
object assigned as the context value.
Using this pattern, any children passed to our UserSessionProvider
will be able to access our UserSessionContext
.
So where do we put this new component?
We already know that every page of our app is going to need different pieces of the user's session data or to interact with that data in some way. So I think a good place for it would be at the top of our component tree, in our App
component. Let's go update that now.
/src/App.tsx
function App() {
return (
<BrowserRouter>
<UserSessionProvider>
<MainRouter />
</UserSessionProvider>
</BrowserRouter>
)
}
Make sure you add our new UserSessionProvider
INSIDE the BrowserRouter
. This will be important later because we will need to utilize React Routers navigation methods, and these cannot be accessed outside of the BrowserRouter
.
Notice that we are passing MainRouter
as a child to our UserSessionProvider
component. Since our MainRouter
contains all our pages, now all of our pages (and all of their child components) will be able to access our context.
Now let's user our new context!
I think a good place to start would be in the nav. We know that when the user is signed in, they should see their avatar and a "Sign Out" link, and when they're NOT signed in, they should see a "Sign In" link. So let's wire this functionality up.
Inside /src/components/MainNav/index.tsx
we first need to import our new custom hook useUserSession
. (If you didn't add this custom hook, you will need to import useContext
from React, and the UserSessionContext
from /src/contexts/user-session.tsx
instead). With it now imported, we just need to call it inside our Functional Component.
/src/components/MainNav/index.tsx
export const MainNav: FC<IProps> = ({ className }) => {
const userSession = useUserSession();
...
};
Lastly, we can check userSession.isSignedIn
to know what content to render.
The MainNav
component should now look like this:
/src/components/MainNav/index.tsx
export const MainNav: FC<IProps> = ({ className }) => {
const userSession = useUserSession()!;
const renderRightContent = () => {
if (userSession.isSignedIn) {
return (
<button>
Sign Out
</button>
);
}
return (
<Link to='/signin'>
Sign In
</Link>
)
}
return (
<nav className={`main-nav ${className}`}>
<Link to='/' className='logo'>
Xample
</Link>
{ renderRightContent() }
</nav>
);
};
We should also update our MainNav
styles to include our new "Sign Out" button.
/src/components/MainNav/styles.css
.main-nav {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: space-between;
align-items: center;
width: 100vw;
height: 5rem;
padding: 0 1rem;
background-color: #136f93;
}
.main-nav a,
.main-nav button {
font-family: Arial, Helvetica, sans-serif;
text-decoration: none;
}
.main-nav a:hover,
.main-nav a:focus-within,
.main-nav button:hover,
.main-nav button:focus-within {
text-decoration: underline;
}
.main-nav a:hover,
.main-nav button:hover {
cursor: pointer;
}
.main-nav .logo {
font-size: 3rem;
}
.main-nav .sign-out {
background: none;
border: none;
}
If we now run npm run dev
the app should still look the same, with a "Sign In" link on the right hand side of the nav. This is because in /src/contexts/use-session.tsx
we hard coded isSignedIn
to be false. If we now go change it to true
, we should see a new "Sign Out" button being displayed on the right side of the nav bar.
Great work!
Refer to branch 004-initial_context if you are having any issues.
Signing In
We have our context wired up and working, but rather than hard coding if the user is signed in our not, we're going to want to allow the user to sign in themselves. Let's work on that next.
First, we're going to need a form for the user to enter and submit their username and password. For this, we're going to need a couple of inputs, and a button. Let's build those components first.
Create a new directory inside /src/components
called InputField
, add a new index.tsx
file, and add the following code to it:
/src/components/InputField/index.tsx
import { FC, InputHTMLAttributes } from 'react';
import './styles.css';
interface IProps {
className?: string;
id: string;
inputProps: InputHTMLAttributes<HTMLInputElement>;
label?: string | JSX.Element;
}
export const InputField: FC<IProps> = ({
className,
id,
label,
inputProps,
}) => {
return (
<div className={`input-field ${className}`}>
{ label && <label htmlFor={id}>{label}</label> }
<input
id={id}
{...inputProps}
/>
</div>
)
}
Let's give it a little styling by creating /src/components/InputField/styles.css
and adding the following to it:
/src/components/InputField/styles.css
.input-field {
display: flex;
flex-direction: column;
margin: 4px;
}
.input-field * {
font-family: Arial, Helvetica, sans-serif;
}
.input-field label {
font-size: 1.2rem;
}
.input-field input {
padding: 0.5rem;
font-size: 1.2rem;
border: 1px solid #3e3e3e;
border-radius: 4px;
background: #595959;
outline: none;
}
.input-field input:focus-within {
border: 1px solid #136f93;
outline: 1px dashed #136f93;
outline-offset: 1px;
}
Next, create a new directory inside /src/components
called Button
, add a new index.tsx
file, and add the following code to it:
/src/components/Button/index.tsx
import { ButtonHTMLAttributes, FC, ReactNode } from 'react';
import './styles.css';
interface IProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
}
export const Button: FC<IProps> = ({
children,
className,
...props
}) => {
return (
<button
className={`button ${className}`}
{ ...props }
>
{ children }
</button>
);
}
Let's also give our new button some styling. Create /src/components/Button/styles.css
and add the following styles to it:
/src/components/Button/styles.css
.button {
margin: 4px;
padding: 0.5rem 1rem;
font-family: Arial, Helvetica, sans-serif;
font-size: 1rem;
border: none;
border-radius: 0.5rem;
background-color: #136f93;
color: #fff;
cursor: pointer;
outline: none
}
.button:not(:disabled):hover,
.button:not(:disabled):focus-within {
background-color: #0d4e67;
}
.button:not(:disabled):hover {
cursor: pointer;
}
.button:not(:disabled):focus-within {
outline: 1px dashed #136f93;
outline-offset: 1px;
}
.button:disabled {
opacity: 0.5;
}
.button:disabled:hover {
cursor: not-allowed;
}
With those components now built, let's create a form on our SignIn
screen for the user to enter their username and password.
We could make a separate component for this, but since this is the only place the user will sign in to our app, I think it's okay if we just do it in the SignIn
screen.
/src/screens/SignIn/index.tsx
export const SignIn: FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback((e) => {
e.preventDefault();
console.log('Signing in...', username, password);
}, [username, password]);
return (
<Screen className='sign-in-screen'>
<form className='signin-form' onSubmit={onSubmit}>
<h1>Sign In</h1>
<InputField
id='username'
label='Username'
inputProps={{
type: 'text',
placeholder: 'Enter your username',
value: username,
onChange: (e) => setUsername(e.target.value),
}}
/>
<InputField
id='password'
label='Password'
inputProps={{
type: 'password',
placeholder: 'Enter your password',
value: password,
onChange: (e) => setPassword(e.target.value),
}}
/>
<div className='buttons-container'>
<Button disabled={ !username || !password}>
Sign In
</Button>
</div>
</form>
</Screen>
);
}
/src/screens/SignIn/styles.css
.signin-form {
display: block;
max-width: 30rem;
margin: 5rem auto;
}
.signin-form h1 {
margin-bottom: 1rem;
}
.signin-form .input-field {
margin-bottom: 1rem;
}
.signin-form .buttons-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
Now that we have a form, let's add a function to our UserSessionContext
where the user's data can be submitted when they click the "Sign In" button.
First, we'll need a couple pieces of state...one to hold our user data, and one to store if the sign in request is processing or not.
/src/contexts/user-session.tsx
const [data, setData] = useState<IUserData | null>(null);
const [processing, setProcessing] = useState(false);
Then we'll need a function that will take the user's username and password, and make the request to the API to sign the user in.
We aren't really going to make a request here. Instead, we're just going to use a setTimeout
to simulate a request. Then we'll manually set the use's data.
/src/contexts/user-session.tsx
const signin = useCallback((username: string, password: string) => {
setProcessing(true);
// using setTimeout to simulate a request
console.log('POST /api/signin', { username, password });
setTimeout(() => {
setData({
id: Math.random().toString(),
username,
});
setProcessing(false);
}, 500);
}, []);
Once our fake request is done and the user's data has been set, we want to automatically redirect the user to the Home
screen. To do this, we'll use React Router's useNavigate
hook and call navigate
as the last statement in our signup
function.
Lastly, we need to add this new data to our userSession
object so that the rest of our app can access these new properties and method:
/src/contexts/user-session.tsx
...
interface IUserSession {
data: IUserData | null;
isProcessing: boolean;
isSignedIn: boolean;
signin: (username: string, password: string) => void;
}
...
const userSession: IUserSession = useMemo(() => ({
data,
isProcessing: processing,
isSignedIn: !!data?.id,
signin,
}), [
data,
processing,
signin,
]);
...
Putting it all together:
/src/contexts/user-session.tsx
interface IUserSession {
data: IUserData | null;
isProcessing: boolean;
isSignedIn: boolean;
signin: (username: string, password: string) => void;
}
...
export const UserSessionProvider: FC<IProps> = ({ children }) => {
const navigate = useNavigate();
const [data, setData] = useState<IUserData | null>(null);
const [processing, setProcessing] = useState(false);
const signin = useCallback((username: string, password: string) => {
setProcessing(true);
// using setTimeout to simulate a request
console.log('POST /api/signin', { username, password });
setTimeout(() => {
setData({
id: Math.random().toString(),
username,
});
setProcessing(false);
navigate('/');
}, 500);
}, []);
const userSession: IUserSession = useMemo(() => ({
data,
isProcessing: processing,
isSignedIn: !!data?.id,
signin,
}), [
data,
processing,
signin,
]);
return (
<UserSessionContext.Provider value={ userSession }>
{ children }
</UserSessionContext.Provider>
);
};
All that's left is to call our new signup
function when the user clicks the "Sign In" button.
/src/screens/SignIn/index.tsx
export const SignIn: FC = () => {
const userSession = useUserSession();
...
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback((e) => {
e.preventDefault();
userSession?.signin(username, password);
}, [username, password]);
...
}
There we go, now we can enter a username and password (you can use any username and password you want), click the "Sign In" button, and we are automatically redirected to the Home
screen. Notice that our "Sign In" link in the nav automatically updates to "Sign Out" because we're now signed in.
Refer to branch 005-signing_in if you are having any issues.
Adding an Avatar
Now that we're able to sign in, and we have the user's data, we can add an avatar to the nav.
Setting up real avatar uploading is outside the scope of this post, so here we're just going to use the first letter of the user's username.
Let's first create a new Avatar
component.
Create a new directory inside /src/components
called Avatar
, add a new file inside it called index.tsx
, and add the following to it:
/src/components/Avatar/index.tsx
import { FC } from "react";
import "./styles.css";
interface IProps {
src?: string;
username: string;
}
export const Avatar: FC<IProps> = ({ src, username }) => {
if (src) {
return (
<div className='avatar'>
<img src={src} alt={`${username}'s avatar`} />
</div>
);
}
return <div className='avatar default-avatar'>{username[0]}</div>;
}
And the styles...
/src/components/Avatar/styles.css
.avatar {
width: 3rem;
height: 3rem;
border: 2px solid #136f93;
border-radius: 50%;
background-color: #000;
overflow: hidden;
}
.default-avatar {
display: flex;
justify-content: center;
align-items: center;
font-size: 100%;
text-transform: uppercase;
}
Now let's add it to our MainNav
.
/src/components/MainNav/index.tsx
...
if (userSession.isSignedIn) {
return (
<div className='user-data-container'>
<button className='sign-out'>
Sign Out
</button>
<Link to='/profile'>
<Avatar username={ userSession.data?.username! } />
</Link>
</div>
);
}
...
/src/components/MainNav/styles.css
...
.main-nav .user-data-container {
display: flex;
align-items: center;
}
.main-nav .user-data-container > *:not(:last-child) {
margin-right: 1rem;
}
There we go! We now have an Avatar displayed to the user when they are logged in. And when it's clicked, the user is redirected to their Profile
screen!
Refer to branch 006-avatar if you're having any issues.
Signing Out
We now need to allow the user to sign out of our application. This process will be similar to what we did with the sign in functionality.
Let's go!
Inside /src/contexts/user-session.tsx
, let's add a new signout
function. Just like in the signin
function, we're going to use a setTimeout
to simulate a network request being made.
/src/contexts/user-session.tsx
...
interface IUserSession {
...
signout: () => void;
}
...
const signout = useCallback(() => {
setProcessing(true);
// using setTimeout to simulate a request
console.log('POST /api/signout');
setTimeout(() => {
setData(null);
setProcessing(false);
navigate('/signin');
}, 500);
}, []);
...
const userSession: IUserSession = useMemo(() => ({
...
signout,
}), [
...
signout,
]);
...
And now we can call signout
when the "Sign Out" button is clicked in the nav.
/src/components/MainNav/index.tsx
...
<button className='sign-out' onClick={userSession.signout}>
...
Way to go! The user is now able to sign out of our application!
Refer to branch 007-signing_out if you're having any issues.
Adding a Private Route
As I mentioned earlier, the /profile
route should only be accessible when the user is signed in. Currently, however, if the user were to manualy enter the /profile
path into the address bar, they would still be able to view the page, regardless if they're signed in or not.
Let's fix this by making the /profile
route private.
Inside of /src/routers
, let create a new component called PrivateRoute
with the following code:
/src/routers/PrivateRoute/index.tsx
import { Navigate, Outlet } from "react-router";
import { useUserSession } from "../../contexts/user-session";
export const PrivateRoute = () => {
const userSession = useUserSession()!;
return userSession.isSignedIn ? <Outlet /> : <Navigate to="/signin" />;
}
It's a very simple component. All we are doing is checking if the user is signed in. If they are, the user is allowed to proceed to requested page. Otherwise, they are navigated to the /signin
screen.
Now we need to updated our main router to use the new PrivateRoute
.
/src/routers/main.tsx
export const MainRouter = () => {
return (
<Routes>
<Route path='/' element={ <Home /> } />
<Route path='/signin' element={ <SignIn /> } />
<Route element={ <PrivateRoute />}>
<Route path='/profile' element={ <Profile /> } />
</Route>
<Route path='*' element={ <NotFound /> } />
</Routes>
);
};
Voila! If we now manually enter /profile
into the address bar when we are not signed in, we are redirected to /signin
. And when we ARE signed in, we can still access /profile
.
Nice job!
Refer to branch 008-private_route if you're having any issues.
Conclusion
Congratulations on making it through this little project with me!
There are still many optimizations that we could make to this application, but I think we're at a good stopping point.
I hope this post helped you gain a better understanding of how to share data across a React application using React Context.
Thank you for coding with me. Happy Hacking!
Posted on February 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 14, 2024