Nextjs and Context API
Asked Answered
C

2

27

Working with Next.js and I am trying to save data inside Context API state after fetching them within getInitialProps, to fix props drilling.

But since getInitialProps is a static method, we can't access it via this.context. I managed to save them within componentDidMount, but in that case, the Context state is empty on the first-page load until it populates. Not sure what the best practice would be in this case. In which lifecycle should I save initial data to Context in order to have them right away like with props passing?

Conceptacle answered 10/1, 2019 at 11:23 Comment(4)
Have you solved this question?Sisera
Cant remember, but I dont think so. I moved to redux. Or yet better if u can do loading until data is ready.Conceptacle
I solved this with ContextApi right now. I'm going to post an example.Sisera
Prop drilling isn't bad. It shouldn't be the only reason you're using context.Sulfapyridine
S
32

you cannot use ContextAPI in Next.js server-side (SSR), because it's against hooks rules. https://reactjs.org/warnings/invalid-hook-call-warning.html

React will run getInitialProps first, so the best solution is to fetch data in there and passing it through your Component using ContextAPI.

Let's go ahead and see it working as below:

Create your AppProvider component

Implement your context provider functions you want to pass through your React components.

For this case, we'll create our global Context provider wrapping the entire application in it.

const AppProvider = ({ children }) => {
  const [galleryData, setGalleryData] = React.useState([]);

  const handleGalleryData = galleryData => {
    setGalleryData(galleryData);
  }

  const contextProps = {
    galleryData,
    handleGalleryData
  };

  return (
    <AppContext.Provider value={contextProps}>
      {children}
    </AppContext.Provider>
  );
}

Then wrap your app with this new provider.

<AppProvider>
  <App />
</AppProvider>

And into your pages, such as index.js, try this way:

Index.getInitialProps = async (props) => {
  const { req, res, query, ...others } = props;

  // use your env variables, endpoint URIs
  // ..

  ... fetch whatever you want..
  const galleryProps = await fetch(endpoint); // isomorphic-unfetch

  return {
    galleryProps,
    query,
    ...others
  };
}

Depending on your Next.js version, you might use getServerSideProps instead of getInitialProps, but be aware of calling it by each request.

Next.js will pre-render this page on each request using the data returned by getServerSideProps Data Fetching docs

Start using ContextAPI over your components

Then in your components, you can check for this data and store it into ContextAPI

const Index = props => {
  const { galleryProps, query, ...others } = props;
  const [galleryData, setGalleryData] = useState(galleryProps);
  const { handleGalleryData, ...contextRest } = useContext(AppContext);
  ...

  // Here you're going to store data into ContextAPI appropriatly.
  useEffect(() => {
    if (typeof galleryProps === 'object' && _.keys(galleryProps).length > 0) {
      handleGalleryData(galleryProps);
    }
  }, [handleGalleryData]);

  // Other times your page is loaded, you will GET this data from ContextAPI, instead of SSR props.
  useEffect(() => {
    if (_.keys(galleryDataProps).length <= 0 && _.keys(contextRest.galleryData).length > 0) {
      setGalleryData(contextRest.galleryData);
    }
  }, []);

....

return (
  <div>
    {JSON.stringify(galleryData)}
  </div>
);

The use case above isn't the best one, but it brings an understanding of how things work with ContextAPI in Next.js applications. I'll explain it below:

  • The first useEffect() is verifying if the component received a data object from props storing it over ContextAPI.

  • The second one checks if the store got some data

You may fetch data in SSR mode over getInitialProps before your component loads.

References

Sisera answered 18/12, 2019 at 13:49 Comment(7)
This is good example which I am currently looking for, So can you give the example in codesandbox (or any other) link with the above implementation in real time which will be helpful.. And I also have doubt whether to implement redux or context api in nextjs 9 to manage the state, So can you explain that as well please??Provoke
Sure, but I'm trying something better, so I can override the serverRuntimeConfig in my pages.Sisera
I am starting with next.js so I am expecting to start with a good project structure and also managing the state.. So I hope to continue with the example link you are providing so please give me the updated solution with good example.. Thanks in advance..Provoke
I just updated my solution, so you can check it out. It's so easy than the React docs. Give it a try. Have a good code! :))Sisera
@GMaiolo, it's the same approach, but getServerSideProps will be called on each request.Sisera
Can you provide a codesandbox or jsfiddle with a working exampleKlausenburg
Hi @Vash, here's an official example of Next.js w/ Context-API: github.com/vercel/next.js/tree/canary/examples/with-context-apiSisera
B
4

When you talk about getInitialProps you are talking about Server Sider Render (SSR). If you don't need to do SSR, the example in Next with Context API is enough, otherwise you can use the pageProps in the file _app.js to initialize your Context, read more about Custom App in documentation Custom App

Note: If you're using Next.js 9.3 or newer, we recommend that you use getStaticProps or getServerSideProps instead of getInitialProps.

import { AppProvider } from '../contexts/AppProvider';

function MyApp({ Component, pageProps }) {
  return (
    <AppProvider initialData={pageProps?.initialData}>
      <Component {...pageProps} />
    </AppProvider>
  );
}

export default MyApp;

Then you can initialize your Context with the data obtained by the server.

import { useState, createContext, useMemo } from 'react';

export const AppContext = createContext();

export const AppProvider = ({ children, initialData }) => {
  const [data, setData] = useState(initialData);

  const value = useMemo(() => ({ data, setData }), [data]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

But wait!!, how can we get the data from the server of the Page? This can be a bit confusing, but pageProps obtained from the Page with getServerSideProps are always passed through MyApp and then to the child component.

getServerSideProps (in Page) ===> MyApp({ Component, pageProps }) ===> Page({pageProps})

And this is what Page would look like consuming the context. The first time, the Server renders the page and initializes the Context, and then you can get the data or update the context again.


import { useContext } from 'react';
import { AppContext } from '../contexts/AppProvider';

export default function Index() {
  const { data, setData } = useContext(AppContext);

  const handleOnClick = () => {
    setData(`Data from client: ${Date.now()}`);
  };

  console.log(data);

  return (
    <div>
      <div>{JSON.stringify(data)}</div>
      <button onClick={handleOnClick}>Update Context</button>
    </div>
  );
}

export function getServerSideProps() {
  const data = `Data from server: ${Date.now()}`;

  return {
    props: {
      initialData: data,
    },
  };
}

You can check that the console.log(data); is displayed on the server and client console, but then only on the client.

You can view the example online here

Bearnard answered 31/12, 2021 at 22:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.