reactjsauthenticationreact-hooksreact-routercreatecontext

Why is context not passing children elements of React context?


I am using react-router to create 2 pages. The first page is a login page. If the user clicks the google Sign up button created through npm module called google-button, then they get signed into their account and navigated to account route.

I am trying to pass a name through the context hook but am unable to view the name in account page.

I am re-routed to the account page but receive no value for name. This is what I am getting in the console of account page --->

Context value in Account: undefined

I am really stuck and have no idea what to do.

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

import App from "./App";
import Account from "./components/Account";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />
  },
  {
    path: "/account",
    element: <Account />
  }
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <RouterProvider router={router} />
);

App.js

import React, { useState, useEffect, createContext } from 'react';
import GoogleButton from 'react-google-button';
import { Navigate } from 'react-router';

export const UserNameContext = createContext();

const App = () => {
  const [user, setUser] = useState(false);
  const name = "Lea";

  const handleClick = () => {
    setUser(!user);
  };

  useEffect(() => {
    console.log("user value changed");
  }, [user]);

  return (
    <UserNameContext.Provider value={name}>
      <div>
        <h1>Sign In</h1>
        <GoogleButton onClick={handleClick} />
        {user ? <Navigate to="/account" /> : "User is Signed Out"}
      </div>
    </UserNameContext.Provider>
  );
}

export default App;

Account.js file -

import React, { useContext } from 'react';
import { UserNameContext } from "../App.js";

const Account = () => {
  const name = useContext(UserNameContext);

  console.log("Context value in Account:", name);

  return (
    <div>
      <h1>Account page</h1>
      <p>{name}</p>
    </div>
  );
}

export default Account;

Solution

  • If you would like the UserNameContext context value to be accessible outside the App component's sub-ReactTree then UserNameContext.Provider needs to be rendered higher in the ReactTree.

    Suggestion is to move the state and UserNameContext.Provider to the index file and provide the context to the sub-ReactTree rendering the routes and routed components, e.g. App and Account.

    Create a provider component to hold and provide the context value:

    UserNameProvider

    import React, {
      useContext,
      useState,
      useEffect,
      createContext
    } from 'react';
    
    export const UserNameContext = createContext();
    
    export useUserNameContext = () => useContext(UserNameContext);
    
    const UserNameProvider = ({ children }) => {
      const [user, setUser] = useState(false);
      const name = "Lea";
    
      const handleClick = () => {
        setUser(!user);
      };
    
      useEffect(() => {
        console.log("user value changed");
      }, [user]);
    
      return (
        <UserNameContext.Provider value={{ name, user, handleClick }}>
          {children}
        </UserNameContext.Provider>
      );
    }
    
    export default UserNameProvider;
    

    App

    import React from 'react';
    import GoogleButton from 'react-google-button';
    import { Navigate } from 'react-router';
    import { useUserNameContext } from '../path/to/UserNameProvider';
    
    const App = () => {
      const { user, handleClick } = useUserNameContext();
    
      return (
        <div>
          <h1>Sign In</h1>
          <GoogleButton onClick={handleClick} />
          {user ? <Navigate to="/account" /> : "User is Signed Out"}
        </div>
      );
    }
    
    export default App;
    

    index.js

    Create a layout route component to render the UserNameProvider component and an Outlet for nested routes to render their content into.

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    import {
      createBrowserRouter,
      RouterProvider,
      Outlet
    } from 'react-router-dom';
    
    import UserNameProvider from '../path/to/UserNameProvider';
    import App from "./App";
    import Account from "./components/Account";
    
    const UserNameProviderLayout = () => (
      <UserNameProvider>
        <Outlet />
      </UserNameProvider>
    );
    
    const router = createBrowserRouter([
      {
        element: <UserNameProviderLayout />,
        children: [
          {
            path: "/",
            element: <App />
          },
          {
            path: "/account",
            element: <Account />
          }
        ],
      },
    ]);
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <RouterProvider router={router} />
    );
    

    Account

    import React from 'react';
    import { useUserNameContext } from '../path/to/UserNameProvider';
    
    const Account = () => {
      const { name } = useUserNameContext();
    
      console.log("Context value in Account:", name);
    
      return (
        <div>
          <h1>Account page</h1>
          <p>{name}</p>
        </div>
      );
    }
    
    export default Account;