skip to Main Content

I am trying to do very basic stuff with React Context to learn it. I’m not sure if/how this complicates things, but I’m doing this in a NextJS 13 project. (I know server components are introduced here but I’m trying to simplify things by labeling everything with 'use client'. The goal is just to have a React Context with a simple string in it, and a function that allows you to change that string. Here is the code in question:

Small file to define the TS type associated with my context’s shape:

'use client'

export type UserContextType = {
  updateUsername: (username: string) => void;
  username: string;
}

My top level layout.tsx component (the layout from which all other pages are rendered):

'use client'

import './globals.css'
import { Inter } from 'next/font/google'
import { createContext, useState } from 'react'

import { UserContextType } from '@/contexts/UserContext'

const inter = Inter({ subsets: ['latin'] })

export const UserContext = createContext<UserContextType>({
  username: '',
  updateUsername: (newName: string) => { console.log(`needs implementation`); }
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const [username, setUsername] = useState<string>('');

  function updateUsername(_username: string) {
    setUsername(_username);
  }

  return (
    <html lang="en">
      <body className={inter.className}>
        <UserContext.Provider value={{username, updateUsername}}>
          {children}
        </UserContext.Provider>
      </body>
    </html>
  )
}

My "Login" component (so named because I want to expand this to a real project at some point):

'use client'

import React, { ReactNode, useContext, useState } from 'react';

import { UserContext } from '@/app/layout';

const Login = (): JSX.Element => {
  const [content, setContent] = useState<string>('');

  const userContext = useContext(UserContext);
  const username = userContext?.username;
  const updateUsername = userContext?.updateUsername;

  function click() {
    updateUsername(content);
  }

  return (
    <div>
      {username && <>Logged in as {username}</>}
      <input
        onChange={(evt) => setContent(evt.target.value)}
        value={content}
      />
      <button
        onClick={() => click()}
      >
        Button
      </button>
    </div>
  );
};

export default Login;

I can read the initial values of the Context from createContext... but not the values passed to the Provider. In other words, in the Login component I can see whatever value I put into the initial value of username and I can run the initial version of updateUsername, in other words I see "needs implementation" in the console. But the values I pass to the provider are ignored.

What am I doing wrong and how does one use React Context to get the provider to pass on correct values?

2

Answers


  1. Chosen as BEST ANSWER

    layout.tsx is not in the component tree with NextJS page route components. For React Context to work, the Provider must be an ancestor of consumers (using useContext). Despite RootLayout’s name and the fact it appears in the default NextJS app along with the default/index page, these red herrings do not mean that it is in the tree as an ancestor of routed NextJS pages. In fact, it isn’t, and that’s why the Provider isn’t actually providing anything to my page.


  2. I suggest you to create another function for Provider: UserContextProvider.

    'use client'
    
    import './globals.css'
    import { Inter } from 'next/font/google'
    import { createContext, useState } from 'react'
    
    import { UserContextType } from '@/contexts/UserContext'
    
    const inter = Inter({ subsets: ['latin'] })
    
    export const UserContext = createContext<UserContextType>({
      username: '',
      updateUsername: (newName: string) => { console.log(`needs implementation`); }
    });
    
    export default UserContextProvider = ({children}: {children: React.ReactNode}) => {
      const [username, setUsername = useState("");
      
      const updateUsername = (_username: string) => {
        setUsername(_username);
      }
      
      return (
        <UserContext.Provider value={{username, updateUsername}}>
          {children}
         </UserContext.Provider>
      );
    }
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <UserContextProvider>
              {children}
            </UserContextProvider>
          </body>
        </html>
      )
    }

    Then, in the Login component, you could do something like this:

    'use client'
    
    import React, { ReactNode, useContext, useState } from 'react';
    
    import { UserContext } from '@/app/layout';
    
    const Login = (): JSX.Element => {
      const {username, updateUsername} = useContext(UserContext);
    
      return (
        <div>
          {username && <>Logged in as {username}</>}
          <input
            onChange={(evt) => updateUsername(evt.target.value)}
            value={username}
          />
        </div>
      );
    };
    
    export default Login;
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search