Search code examples
reactjsnext.jsserver-side-renderingreact-context

Next.js + SSR not updating the Context API


We have a simple setup involving a Context API provider wrapping the _app.tsx page. The flow we're trying to implement is:

  1. On a page we collect some data from an API by using getServerSideProps
  2. We send the data to the page through the props
  3. We update the context from the page (the context provider is wrapping the _app.tsx as mentioned above)
  4. The context is updated and all the children components can access the data in it

So we have a pretty standard setup that works correctly on the client side. The problem is that the updated context values are not being used to SSR the pages.

Context API

const ContextValue = createContext();
const ContextUpdate = createContext();

export const ContextProvider = ({children}) => {
  const [context, setContext] = useState({});
  
  return <ContextValue.Provider value={context}>
    <ContextUpdate.Provider value={setContext}>
      {children}
    </ContextValue.Provider>
  </ContextUpdate.Provider>
}

export const useContextValue = () => {
  const context = useContext(ContextValue);
  return context;
}

export const useContextUpdate = () => {
  const update = useContext(ContextUpdate);
  return update;
}

Then we have on _app.jsx:

...
return <ContextProvider>
  <ContextApiConsumingComponent />
  <Component {...pageProps} />
</Context>

And in any page, if we can update the context by using the hook provided above. For example:

export function Index({customData, isSSR}) {
  const update = useContextUpdate();

  if(isSSR) {
    update({customData})
  }
  return (
    <div>...</div>
  );
}

export async function getServerSideProps(context) {
  ...

  return {
      props: { customData , isSSR}
  };
};

And we can consume the context in our ContextApiConsumingComponent:

export function ContextApiConsumingComponent() {
  const context = useContextValue()

  return <pre>{JSON.stringify(context?.customData ?? {})}</pre>
}

The (very simplified) code above works fine on the client. But during the SSR, if we inspect the HTML sent to the browser by the server, we'll notice that the <pre></pre> tags are empty even though the context values are correctly sent to the browser in the __NEXT_DATA__ script section (they are precisely filled with the data on the browser after the app is loaded).

I've put together a GitHub repo with a minimum reproduction too.

Am I missing something?


Solution

  • Not sure if you have other reasons for using the context to get this data into the api consumer but with nextjs, the getStaticProps or getServerSideProps of course feeds into the route component, but maybe you've missed the following detail?

    The Component prop is the active page, so whenever you navigate between routes, Component will change to the new page. Therefore, any props you send to Component will be received by the page.

    pageProps is an object with the initial props that were preloaded for your page by one of our data fetching methods, otherwise it's an empty object.

    https://nextjs.org/docs/advanced-features/custom-app

    Therefore the _app.tsx can act on any props your route components would normally receive from getStaticProps or getServerSideProps

    types.ts

    
    // or whatever union type you need to indicate that myriad of "custom datas" you might be pushing out of getStaticProps
    export type MaybeCustomDataFromGetStaticProps = Map<string, string> | undefined;
    
    export type PagePropsWithCustomData = {
      customData: MaybeCustomDataFromGetStaticProps
      isSSR?: boolean;
    }
    

    pages/_app.tsx

    import type { PagePropsWithCustomData } from '../types';
    
    const App = ({
      Component,
      pageProps: { customData, isSSR, ...pageProps},
      router,
    }: AppProps<PagePropsWithCustomData & Record<string, unknown>>) => {
    
      return (
        <ContextProvider value={{ isSSR, customData }}>
          <ContextApiConsumingComponent />
          <Component {...pageProps} />
        </ContextProvider>
      )
    
    }
    

    pages/somepage.tsx

    import type { NextPage, GetServerSideProps } from "next";
    import type { PagePropsWithCustomData } from '../types';
    import { magicallyGetCustomData } from '../services/Api';
    import { isSSR } from '../core/Routing';
    
    type IndexRouteProps = PagePropsWithCustomData & {
      isSSR?: boolean;
    }
    
    const IndexRoute:NextPage<IndexRouteProps> = ({ customData, isSSR }) => {
    
      // no longer need to do anything with customData, or isSSR here
    
      return (
        <div>stuff</div>
      );
    }
    
    export const getServerSideProps:GetServerSideProps<IndexRouteProps> = async (context) => {
      const customData = await magicallyGetCustomData();
      
      return {
          props: { customData , isSSR: isSSR(context) }
      };
    };
    
    export default IndexRoute