I'm using React useContext
to avoid prop-drilling, and building static pages in NextJS, as described in this Technouz post (NB: this is not about the NextJS getStaticProps
context
parameter).
The basic functionality is working; however, I can't figure out the right way to update the context from components farther down the chain.
At a high level, I have this:
// pages/_app.js
function MyApp({ Component, pageProps }) {
const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} );
return (
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
// components/Header.js
export default function Header() {
const { headerData } = useHeader();
return (
<header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar />
</header>
)
}
// lib/context/header.js
const HeaderContext = createContext();
export function HeaderProvider({value, children}) {
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
The Navbar
component also uses the context
.
That all works. I query the data from a headless CMS using getStaticProps
, and everything gets passed through pageProps
, and when I run npm run build
, I get all of my static pages with the appropriate headers.
But, now I'm extending things, and not all pages are the same. I use different models at the CMS level, and want to display different headers for landing pages.
Inside of [pages].js
, I handle that thusly:
const Page = ({ data }) => {
switch (data.pageType) {
case 'landing-page':
return (
<PageLandingPage data={data} />
);
case 'page':
default:
return (
<PageStandard data={data} />
);
}
}
Now, if we're building a static landing page instead of a static standard page, the whole hierarchy would look something like this:
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar>
<ul>
{menu && <MenuList type='primary' menuItems={menu.menuItems} />}
</ul>
</Navbar>
</Header>
<PageLandingPage {...pageProps}> // *** Location 2
<LandingPageSection>
<Atf> // *** Location 1
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
</Atf>
</LandingPageSection>
</PageLandingPage>
</HeaderProvider>
Location 1 and Location 2 are where I want to update the context
. I thought I had that working, by doing the following at Location 1:
// components/Atf.js
export default function Atf({content}) {
// this appeared to work
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
return (
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
)
}
I say "thought", because I was, in fact, getting my <UrgentBanner>
component properly rendered on the landing pages. However, when digging into the fact that I can't get it to work at Location 2, I discovered that I was actually getting warnings in the console about "cannot update a component while rendering a different component" (I'll come back to this).
Now to Location 2. I tried to do the same thing here:
// components/PageLandingPage.js
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// this, to me, seems the same as above, but isn't working at all
if (giveawayLandingPage?.headerMenu) {
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
menu: { ...giveawayLandingPage.headerMenu }
}
);
}
return (
<div>
{giveawayLandingPage.lpSection.map(section => <LandingPageSection details={section} key={section.id} />)}
</div>
)
}
To me, that appears that I'm doing the same thing that "worked" in the <Atf>
component, but ... it's not working.
While trying to figure this out, I came across the aforementioned error in the console. Specifically, "Cannot update a component (MyApp
) while rendering a different component (Atf
)." And I guess this is getting to the heart of the problem — something about how/when/in which order NextJS does its rendering when it comes to generating its static pages.
Based on this answer, I initially tried wrapping the call in _app.js
in a useEffect
block:
// pages/_app.js
...
/* const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} ); */
const [ headerData, setHeaderData ] = useState({});
useEffect(() => {
setHeaderData({
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
});
}, []);
But that didn't have any impact. So, based on this other answer, which is more about NextJS, though it's specific to SSR, not initial static page creation, I also wrapped the setState
call in the <Atf>
component at Location 1 in a useEffect
:
// components/Atf.js
...
const { headerData, setHeaderData } = useHeader();
/* setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
) */
useEffect(() => {
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
}, [setHeaderData])
That did stop the warning from appearing in the console ... but it also stopped the functionality from working — it no longer renders my <UrgentBanner>
component on the landing page pages.
I have a moderately good understanding of component rendering in React, but really don't know what NextJS is doing under the covers when it's creating its initial static pages. Clearly I'm doing something wrong, so, how do I get my context
state to update for these different types of static pages?
(I presume that once I know the Right Way to do this, my Location 2 problem will be solved as well).