Next.js context provider wrapping App component with page specific layout component giving undefined data
Asked Answered
W

2

11

I have an auth context component where I'm wrapping my main app component, but at the same time I'm also trying to do page specific layout component per Next.js documentation here: https://nextjs.org/docs/basic-features/layouts#per-page-layouts

Am I doing this correctly, because I can't seem to be getting the data from my Context provider.

/context/AuthContext.js

const UserContext = createContext({});

export default function AuthContext({children}) {
  // .. code
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

export const useUser = () => useContext(UserContext);

/_app.js

function MyApp({ Component, pageProps }) {    

  const getLayout = Component.getLayout || ((page) => page);

  return getLayout(
    <div>
      <AuthContext>
        <Component {...pageProps} />
      </AuthContext>
    </div>
  );
}

export default MyApp;

/components/Project/List.js

import { useUser } from "../../context/AuthContext";

const ProjectList = () => {
  const { user } = useUser();
  console.log("get user data", user);

  return (
    <>
      test
    </>
  );
};

export default ProjectList;

I'm trying to console log the user, but it's giving me undefined. I'm thinking it's because the way it's wrapped as a layout component? I could be doing this wrong. But I did console log inside my AuthContext for user, and the information there is correct.

/pages/projects/index.js

const Projects = () => {
  // code goes here
  return (
    <div>
      code goes here
    </div> 
  )
}

export default Projects;

Projects.getLayout = function getLayout(page) {
  return <ProjectLayout>{page}</ProjectLayout>;
};

When I remove the Projects.getLayout block of code, the data comes back, but when I add this code, data is gone.

/components/Project/Layout.js

const ProjectLayout = ({children}) => {
  return (
    <>
      <ProjectList />
      {children}
    </>
  }

export default ProjectLayout
Wonky answered 6/9, 2021 at 17:6 Comment(0)
G
34

With your current structure ProjectLayout isn't getting wrapped by the AuthContext, meaning you won't have access to its context.

You can modify your _app's structure and move the getLayout call around so that the context wraps it properly.

function MyApp({ Component, pageProps }) {    
    const getLayout = Component.getLayout || ((page) => page);

    return (
        <AuthContext>
            {getLayout(<Component {...pageProps} />)}
        </AuthContext>
    );
}
Guano answered 6/9, 2021 at 19:37 Comment(7)
I have the same problem but with <SWRConfig value={{fetcher: ....}} /> in 'next/swr'. I tried your method but the Layout component still doesn't receive the fetcher. Here are some useful link: nextjs.org/docs/basic-features/layouts#data-fetching and swr.vercel.app/docs/global-configuration Do you have any ideas?Nystagmus
Is the SWRConfig properly wrapping the layout component? Hard to tell what's wrong without seeing your code. You may want to post your issue as a new question.Guano
#69681736Nystagmus
Brilliant, I was having a similar problem while adding Chakra UI's ChakraProvider and ColorModeProvider, and Next Auth's NextAuthProvider to the _app.tsx file. All is working just fine now with this approach. Big thanks @juliomalves!Vaporous
You star @juliomalves, this has been bugging me for months - I didn't realise you could do it like that with the getLayout being wrappedKurzawa
guess it even saves kb in a build using it this way !?Flaring
@Guano a big big like and a ton of applauseAlexandrite
O
3

Note that calling getLayout inside the Context could lead to errors if the getLayout function uses hooks or stuff that depends on a parent element.

It will be calling getLayout first and then the context, so the value of it will be initially the default (fallback) value (it's like doing foo(bar()) and expecting that foo will be called before bar).

To avoid this, return directly the component (using getLayout as a function that generates a component):

// /pages/projects/index.js
Projects.getLayout = (children) => (
  // can't use hooks here, return the component immediately
  <ProjectLayout>{children}</ProjectLayout>;
);

or use the layout as a component:

// /pages/projects/index.js
Projects.Layout = ({ children }) => {
  return <ProjectLayout>{children}</ProjectLayout>;
};

// /pages/_app.js
export default function App({ Component, pageProps }) {    
  const Layout = Component.Layout || (({ children }) => children);
  return (
     <AuthContext>
       <Layout>
          <Component {...pageProps} />
       </Layout>
     </AuthContext>
  );
}

Edit: The difference is more visible without JSX

// incorrect
return React.createElement(AuthContext, null,
  // getLayout is called before AuthContext
  getLayout(
    React.createElement(Component, pageProps)
  )
)

// correct
return React.createElement(AuthContext, null,
  // Layout is called after AuthContext
  React.createElement(Layout, null,
    React.createElement(Component, pageProps)
  )
)
Obau answered 2/11, 2022 at 16:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.