Sharing Data Across your React App with React Context

wraith

Jake Lundberg

Posted on February 12, 2023

Sharing Data Across your React App with React Context

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
Enter fullscreen mode Exit fullscreen mode

then run to install the project's initial dependencies:

cd xample-react-context && npm install
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now if you run npm run dev you should see the following:

Image description

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

/src/screens/Screen/styles.css

.screen-content {
  margin-top: 5rem;
  padding: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

There we go, now if you run npm run dev you should see:

Image description

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.
  • 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 the SignIn 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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();

  ...
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

/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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  }, []);
Enter fullscreen mode Exit fullscreen mode

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,
  ]);

...
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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]);

  ...
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
      );
    }

    ...
Enter fullscreen mode Exit fullscreen mode

/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;
}
Enter fullscreen mode Exit fullscreen mode

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,
  ]);

...
Enter fullscreen mode Exit fullscreen mode

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}>

...
Enter fullscreen mode Exit fullscreen mode

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" />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
wraith
Jake Lundberg

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