I'm taking my first pass at using NextJS, and also my first pass at using a headless CMS (DatoCMS in this case).
Everything was actually working fine, but I found myself using a lot of prop drilling, to get information returned from the CMS down into deeply nested components, and that was becoming cumbersome. After some googling, I decided to try to use React Context to avoid prop drilling as described here.
The first use case is getting site branding info and menu items down to the nav component (this info will almost always be the same, everywhere on the site, but there might be some sections (e.g., landing pages) where they're different).
The problem is, the React useState set function as passed through the ContextProvider doesn't actually seem to set the state. There are several questions on here about state not being updated in various scenarios, but this isn't setting the initial state after NextJS grabs the data from getStaticProps, but here it's not doing it even if I manually call the set function directly in the component wrapped in ContextProvider, never mind somewhere farther down the stack.
As described in that blog post, I have the context provider pulled out into its own component:
// context/header.js
import { createContext, useContext, useState } from 'react';
const HeaderContext = createContext();
export function HeaderProvider({children}) {
const [header, setHeader] = useState(null);
const value = { header, setHeader };
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
Then, in _app.js, I wrap my <Header /> component in the context provider:
// pages/_app.js
import Header from '../components/Header';
import { HeaderProvider } from '../lib/context/header';
function MyApp({ Component, pageProps }) {
return (
<>
<HeaderProvider>
<Header { ...pageProps } />
</HeaderProvider>
<Component {...pageProps} />
</>
)
}
export default MyApp
I'm just starting this out, so right now, everything is designed to be a static page (no server side rendering or client side React yet).
The main page, from index.js, successfully grabs the data from DatoCMS:
// pages/index.js
import { request } from "../lib/datocms";
import { HOMEPAGE_QUERY } from '../lib/query';
export async function getStaticProps() {
const data = await request({
query: HOMEPAGE_QUERY
});
return {
props: { data }
};
}
export default function Home({ data }) {
return (
<div>
This is the home page
</div>
);
}
So far, this is working, because in my <Header /> component, I can log the data passed.
As I understand how NextJS works, that getStaticProps call populates pageProps which get passed back to the Component (the page) to generate the static html. The results of the request call in getStaticProps is in fact making it's way back to pageProps to be used both in the <Component /> and the <Header /> in _app.js, but when I try to use the setHeader() function from header.js, pulled in by calling useHeader(), the value of the stateful header is always null.
// components/Header.js
import React, { useEffect } from 'react'
import { useHeader } from '../lib/context/header';
import Navbar from './Navbar';
export default function Header({data}) {
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
console.log(HeaderData);
// outputs:
// {
// siteName: 'My Site',
// companyLogo: {
// alt: 'My Site Logo',
// height: 113,
// url: 'https://www.datocms-assets.com/<url>',
// width: 122
// },
// menu: { title: 'primary', menuItems: [ [Object], [Object] ] }
// }
const { setHeader } = useHeader();
useEffect(() => {
setHeader(HeaderData)
}, [HeaderData]);
return (
<header>
<Navbar />
</header>
)
}
And then using the context in <Navbar />:
import React from 'react';
import { useHeader } from '../lib/context/header';
export default function Navbar() {
const { header } = useHeader();
console.log(header); // null :(
return (
<div>This is my nav</div>
)
}
How do I actually get my context state into <Navbar />?
The problem is, the header context in <Navbar /> is always the initial state of null. There was a similar question, but it wasn't using NextJS, just React, and the answer there seemed to be that he was trying to use the useContext call outside of the component chain he had wrapped in ContextProvider. Maybe I'm missing something, but I'm pretty sure that's not my issue.
So, I did the natural debugging thing, and started adding a bunch of console.logs. Before I even get to the <Navbar /> component, it appears that the setHeader() call isn't actually updating the state.
Here's an updated <Header />:
export default function Header({data}) {
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
console.log('Set HeaderData to');
console.log(HeaderData);
// In the initial code, I just pulled in setHeader here, but now pulling in header for debugging purposes
const { header, setHeader } = useHeader();
setHeader(HeaderData); // various incantations here
console.log('Just set header, header is');
console.log(header)
But no matter what incantation I use at "various incantations here", the result logged to console is always the same:
Set HeaderData to
{
siteName: 'My Site',
companyLogo: {
alt: 'My Site Logo',
height: 113,
url: 'https://www.datocms-assets.com/<url>',
width: 122
},
menu: { title: 'primary', menuItems: [ [Object], [Object] ] }
}
Just set header, header is
null
I've tried setHeader(HeaderData), which seems most analogous to what he did in that original blog post, but since that wasn't working, I also tried setHeader({HeaderData}) and setHeader({...HeaderData}), but the results are identical.
Why isn't this set setting?