Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context provided in _app.js can't be consumed in pages in SSR #4194

Closed
1 task done
jaydenseric opened this issue Apr 23, 2018 · 56 comments · Fixed by #4639
Closed
1 task done

Context provided in _app.js can't be consumed in pages in SSR #4194

jaydenseric opened this issue Apr 23, 2018 · 56 comments · Fixed by #4639
Assignees
Labels
kind: bug Confirmed bug that is on the backlog

Comments

@jaydenseric
Copy link
Contributor

jaydenseric commented Apr 23, 2018

React v16.3 context provided in pages/_app.js can be consumed and rendered in pages on the client, but is undefined in SSR. This causes React SSR markup mismatch errors.

Note that context can be universally provided/consumed within pages/_app.js, the issue is specifically when providing context in pages/_app.js and consuming it in a page such as pages/index.js.

  • I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior

Context provided in pages/_app.js should be consumable in pages both on the server for SSR and when browser rendering.

Current Behavior

Context provided in pages/_app.js is undefined when consumed in pages for SSR. It can only be consumed for client rendering.

Steps to Reproduce (for bugs)

In pages/_app.js:

import App, { Container } from 'next/app'
import React from 'react'
import TestContext from '../context'

export default class MyApp extends App {
  render () {
    const { Component, pageProps } = this.props
    return (
      <Container>
        <TestContext.Provider value="Test value.">
          <Component {...pageProps} />
        </TestContext.Provider>
      </Container>
    )
  }
}

In pages/index.js:

import TestContext from '../context'

export default () => (
  <TestContext.Consumer>
    {value => value}
  </TestContext.Consumer>
)

In context.js:

import React from 'react'

export default React.createContext()

Will result in:

screen shot 2018-04-23 at 2 29 29 pm

Context

A large motivation for the pages/_app.js feature is to be able to provide context persistently available across pages. It's unfortunate the current implementation does not support this basic use case.

I'm attempting to isomorphically provide the cookie in context so that graphql-react <Query /> components can get the user's access token to make GraphQL API requests. This approach used to work with separately decorated pages.

Your Environment

Tech Version
next v6.0.0-canary.5
node v9.11.1
@timneutkens
Copy link
Member

Is it because _app.js provides its own context? 🤔

@timneutkens
Copy link
Member

That actually can't be the problem when you nest inside <Container> though 🤔

@timneutkens
Copy link
Member

It's related to React.createContext() creating a local state inside the module, _app.js and pages have different bundles.

@jaydenseric
Copy link
Contributor Author

@timneutkens as the bundling seems to work for the client, does that mean we could split the server bundles in a similar way to the client?

@timneutkens
Copy link
Member

Unfortunately it doesn't work as expected with commonschunk on the server. I have some ideas to fix this once and for all, but haven't had time to implement it yet.

@tz5514
Copy link

tz5514 commented May 7, 2018

Will this be fixed? This problem break lots of behavior from module's local variable, and I think it's a important issue.
ex: react-act https://github.com/pauldijou/redux-act#types

@timneutkens
Copy link
Member

Well it's not really a Next.js issue. More so with webpack, so I need to dig into this pretty deep. If anyone has experience with it please do reach out.

@brad-decker
Copy link

@timneutkens i'm not super familiar but i'm willing to do a deep dive to help figure out a solution. Is there any pointers / additional details you could share to help me get started, i'm gonna get started researching the issue on my own but any knowledge transfer would be helpful.

@timneutkens
Copy link
Member

So, since Next 5 we run webpack 2 times, once for the server, once for the client.
Both have different configurations.

On the server webpack will pack every page into it's own bundle (entrypoint), which ends up in .next/dist. This bundle holds everything, including all imported components etc.

Say you have 2 pages that both import Header it will end up in both page 1 and page 2's bundle.

Because of this module state / singletons don't work, since if you server render page 1, and it holds a module state inside the component, for example:

let timesRendered = 0

export default () => {
  timesRendered++
  return <div>test</div>
}

It won't shared between server rendering of 2 pages.

From what I've understood looking at the new context API, this is exactly what the context API depends on (local module state). So if you have _app.js and you initialize it there, and then try to access the context from pages/index.js, they'll have 2 different context module states.

I hope this explains the issue.

I'm not entirely sure how we can fix this issue. In Next 4 and below we emitted every possible file into .next/dist, and then, because we only applied Babel, the paths when importing were still correct, but with webpack this works a little different, since it parses import/export and turns it into a dependency tree, and also bundles to a file, not 1-1 into the specified directory.

@brad-decker
Copy link

@timneutkens thanks for this writeup. i'm digging in now and might have additional questions for you as I work through it.

@joeporpeglia
Copy link

Is it possible to create a similar "main" commons chunk for the server bundles? This issue doesn't seem like a problem for the client bundles (.next/bundles/pages/*.js and .next/static/commons/main-*.js) since the createContext() call only exists in the main chunk. I haven't been able to get a working example, but I'll keep messing around with the idea.

@brad-decker
Copy link

@joeporpeglia @timneutkens -> I used dynamic to import the context provider in the app and the consumer in the page to test how that modified the bundle. As expected the code related to the consumer initialization was put into its own chunk in .next/dist but the problem isn't solved. I gave my context consumer a obnoxious name to help with searching the dist files.

import React, { Component, createContext } from 'react';

const BradContext = createContext({ x: 3 });

export class BradProvider extends Component {
  state = {
    x: 5,
  };
  render() {
    return (
      <BradContext.Provider value={this.state}>
        {this.props.children}
      </BradContext.Provider>
    )
  }
}

export const BradConsumer = BradContext.Consumer;

The state is kept entirely within this file.

In _app.js

import App, { Container } from 'next/app';
import dynamic from 'next/dynamic';

const BradProvider = dynamic(import('../components/context').then(r => r.BradProvider));

export default class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props;
    return (
      <Container>
        <BradProvider>
          <Component {...pageProps} />
        </BradProvider>
      </Container>
    )
  }
}

in index.js:

import React, { Component } from 'react';
import dynamic from 'next/dynamic';

const BradConsumer = dynamic(import('../components/context').then(r => r.BradConsumer));

export default class Test extends Component {
  render() {
    return (
        <BradConsumer>
          {value => <span>{value.x}</span>}
        </BradConsumer>
    )
  }
}

With code splitting and module state, if the module is entirely self contained within a chunk does it solve the problem you were outlining @timneutkens -- i can confirm that the only time the state for the context is referenced within the compiled code is in the chunk and not within the page or the app computed entry points. Or is it the case that both times it is imported from the various entry points that the module state is essentially initialized again so that even though the code is split out each entry point is creating its own in-memory allocation for the code contained in the common chunk?

@brad-decker
Copy link

With the above implementation the server renders "3", on the client there is a flash of "loading" followed by "5".

@brad-decker
Copy link

brad-decker commented May 8, 2018

@timneutkens is the client side bundle just in .next/bundle versus server side in .next/dist/bundle? I'm trying to see how each of these differ in terms of its implementation of the component tree as it relates to the context (specifically the dynamic import variation I did above) and they seem identical. Its as if when the Consumer is being mounted it doesn't see the Provider in the same tree.

@joeporpeglia
Copy link

joeporpeglia commented May 8, 2018

Or is it the case that both times it is imported from the various entry points that the module state is essentially initialized again so that even though the code is split out each entry point is creating its own in-memory allocation for the code contained in the common chunk?

Yeah this is the root problem here. For the server compilation only, both index.js and _app.js are separately bundled into two commonjs modules that are run in node. This server compilation also uses a special webpack "externals" condition so that all non-source modules (basically anything from node_modules that isn't nextjs) are resolved through regular node require calls. This prevents webpack from bundling modules into index.js and _app.js that already work in a nodejs env. However, your 'components/context' file does not match that "externals" condition (and shouldn't since it must be transpiled). This causes the module to be included in both of the commonjs modules produced for index.js and _app.js.

Unfortunately I don't think there's any way to create a commons chunk and have it load properly when targeting node. Maybe another solution could involve bundling _app.js and _document.js with each page module. I think that would also require some changes to how we import the app, document, and requested page.

Edit to show how that might look:

Instead of importing app, document, and page as separate modules, we could destructure all three from a single page bundle:

let { Page, Document, App } = await requirePage(page, {dir, dist});

@brad-decker
Copy link

If there were a way to build a commons chunk look like in my example? Would it exist in the chunks/ folder inside dist? And how would it differ from the one generated by using dynamic imports

@joeporpeglia
Copy link

@brad-decker I don't believe there's any way to make commons chunks work when targeting node 😞. I also don't think the dynamic imports you provided in that example would work as long as the server renderer require's _app.js and the currently requested page separately. https://github.com/zeit/next.js/blob/aec4c00214a82097d07295b00f0af5f4eb24167b/server/render.js#L59-L66

@steida
Copy link
Contributor

steida commented May 8, 2018

Sorry to disturb, but why _app is a thing? I have own app component, and context is preserved correctly. What am I missing?

@joeporpeglia
Copy link

If you render that custom app component as the root in each of your pages/ then it'll remount when navigating to another page on the client. This makes it difficult to add things like page transitions or global react state across different pages.

@mtford90
Copy link

mtford90 commented May 9, 2018

For now i've worked around this issue by avoiding _app.js and wrapping every single page like so:

// lib/page.js
import * as React from "react";
import urlContext from "./urlContext";

export default (PageComponent) => {
  return class Page extends React.Component {
    public static async getInitialProps(initialProps) {
      const { req } = initialProps
      const url = req && req.url;
      let props = { url };

      const getInitialProps = PageComponent.getInitialProps;

      if (getInitialProps) {
        props = {
          ...props,
          ...getInitialProps(initialProps)
        }
      }

      return props;
    }

    public render() {
      const url = this.props.url;

      return (
        <urlContext.Provider value={url}>
          <PageComponent/>
        </urlContext.Provider>
      );
    }
  }
}

// pages/index.js
import page from 'lib/page.js'

export default page(() => {
    return (
        <div>A page</div>
    )
})

@brad-decker
Copy link

@mtford90 yeah, i created a HoC as well for setting up context, but its not clean. I actually refactored away from a HoC when i upgraded to 6 and saw the potential there for the app to handle that for us.

@elrumordelaluz
Copy link
Contributor

here is a quick repro with the current behaviour in case could be useful for debug/ try stuff

@timneutkens
Copy link
Member

timneutkens commented May 17, 2018

@RustyDev that's something entirely different. What this does is pass reduxStore to ctx of pages. So that inside pages/index.js you can do:

export default class extends React.Component {
  static async getInitialProps({ reduxStore }) {
    await reduxStore.dispatch(/*something*/)
    return {}
  }
}

@lasersox
Copy link

@joeporpeglia @timneutkens I ran into this last night and spent a couple hours banging my head on the wall. @mtford90 thanks for suggesting the workaround.

Is there any proposal that could resolve this issue?

@Ilaiwi
Copy link

Ilaiwi commented May 30, 2018

Hi, any updates on this issue?

@martinjlowm
Copy link

martinjlowm commented Jul 31, 2018

I'm not entirely sure this is fixed, the shared module part, yes, but not the context behavior.

I just upgraded to 6.1.1-canary.3 (which includes #4639) and it still appears that a provider can fire before a consumer is rendered, resulting in the consumer's function to never render its children.

Compared to the browser, reactContext._currentValue appears to remain equal to reactContext._defaultValue until the consumer is initially rendered.

When Next.js renders, reactContext._currentValue is updated before the consumer element is rendered.

I have a simple case as the follows (yet I haven't tested exactly this example):

                                     <- 1.
<reactContext.Provider value="Test">
                                     <- 2.
  <reactContext.Consumer>
    {value => value}
  </reactContext.Consumer>
</reactContext.Provider>

In the browser, _currentValue is equal to _defaultValue in 1. and 2.

When Next.js renders, _currentValue is equal to _defaultValue in 1. and equal to "Test" in 2.

edit: I have just tested a small example similar to the one listed above and that one works. The only obvious difference between this simple one and the piece of code I have which doesn't work, is that the latter is an external Node module that's pre-transpiled into createElement's with tsc.

edit 2: well... false alarm... so I figured out the issue. A Hidden element of Material UI (https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Hidden/Hidden.js) had snuck its way into the code. What wasn't obvious, was that the implementation apparently defaults to a JavaScript version which would never render its children. I'd expect it to default to CSS - apparently not.

@gaearon
Copy link

gaearon commented Aug 2, 2018

@timneutkens Thank you for jumping on this so quickly!

@timneutkens
Copy link
Member

timneutkens commented Aug 2, 2018

@gaearon happy to! was an interesting one to solve 😄

zarathustra323 added a commit to zarathustra323/fortnight-web that referenced this issue Aug 8, 2018
Fixes issue with `React.createContext()` per vercel/next.js#4194
@fi5u
Copy link

fi5u commented Sep 19, 2018

@timneutkens With this fix, is Context.Provider only permitted to be used from _app.js? For me, it works when I set the provider in _app.js, but Context.Consumer does not get the values when Context.Provider is defined in a file in pages. Is this by design? Ideally, I'd like to set Context.Provider at the page, not app, level, then use Context.Consumer in child components.

Next version: 7.0.0

@thomhos
Copy link
Contributor

thomhos commented Oct 4, 2018

Just chiming in here. I'm currently experiencing this issue on next 6.1.2 using zeit/next-typescript.

I've created a file which creates a context:

export interface TranslationContextProperties {
  language: string;
  translations: TranslationMap;
}

export const TranslationContext = React.createContext<TranslationContextProperties>({
  language: 'en',
  translations: defaultTranslations,
});

I then import it in _app.tsx and assign a value to TranslationContext.Provider.

If I then use a TranslationContext.Consumer in the same file it works both client and server side.

However, if I import the context and use the Consumer in the index for instance, it only works client side.

Now I'm wondering, should this be fixed on 6.1.2 or only in 7?

@timneutkens
Copy link
Member

Only in 7

@dfoverdx
Copy link

dfoverdx commented Dec 4, 2018

I'm running into this same issue with Next 7.0.2. I found that if I wrap the context into its own component which won't update its state if the value passed as a property is undefined, that will work between pre-fetched page changes, however that has its own issue of propagating values (for some reason, the inner consumer stays one value behind).

Here's a gist of my solution. https://gist.github.com/dfoverdx/4afe0e4ad4bda6702b9debf63ca6cd9c

The one disadvantage to this approach is that it wraps the app's Container in an extra component per Context. If you have several contexts, the tree in the React debugger gets very wide.

@ssylvia
Copy link

ssylvia commented Jan 14, 2019

@timneutkens Also ran into this issue with Next 7.0.2.

@iMerica
Copy link

iMerica commented May 16, 2019

This issue persist for me in Next 8.1.0.

I'm following the official "with redux" example here and I continue to see

[ wait ]  compiling ...
[ error ] ./pages/_app.js
Module not found: Can't resolve 'react-redux' in '/app/pages'

My package.json includes

  "dependencies": {
    "@zeit/next-sass": "^1.0.1",
    "@zeit/next-typescript": "^1.1.1",
    "next": "^8.1.0",
    "node-sass": "^4.12.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.0.3",
    "redux": "^4.0.1"
  }

This only happens after adding _app.js. Regular ./pages/*.js pages worked fine.

@Herlevsen
Copy link

I can also confirm i am having problems with context api in SSR on 8.1.0 and i had to downgrade again.

@medmin
Copy link

medmin commented Oct 1, 2019

Next 9.0.5

I am still NOT able to migrate from React SPA to Nextjs, with the React Context API.

And also, the error message is misleading.

I have the error message like this:

Uncaught Error: Invariant failed: You should not use <withRouter(BaseContainer) /> outside a <Router>.

It took me a week to figure it out that it's NOT the router. It's the Context!!!!

I hope you people at least fix the error message. The router is fine.

Capture.PNG

@timneutkens
Copy link
Member

@medmin please read this thread: https://twitter.com/timneutkens/status/1154351052973625346

This bug does not exist anymore so it must be something in your code, we have tests verifying it works correctly.

@timneutkens
Copy link
Member

@sytona I'm not sure what you're referring to.

@timneutkens
Copy link
Member

That's not true because webpack has it's own implementation of the module singleton system. This implementation does have the issue that the modules object is per-entrypoint, however I wrote a webpack plugin that changes it to share the modules object between entrypoints that solves this issue.

@Ahmdrza
Copy link

Ahmdrza commented Mar 12, 2020

Getting same issue in next 9.2.1. Context API only works if consumer is placed in _app.tsx. It does't work when placed in pages.

@timneutkens
Copy link
Member

Getting same issue in next 9.2.1. Context API only works if consumer is placed in _app.tsx. It does't work when placed in pages.

This comment is not actionable and on a closed issue from almost 2 years ago.

@platocrat
Copy link

platocrat commented Jul 22, 2020

For now i've worked around this issue by avoiding _app.js and wrapping every single page like so:

// lib/page.js
import * as React from "react";
import urlContext from "./urlContext";

export default (PageComponent) => {
  return class Page extends React.Component {
    public static async getInitialProps(initialProps) {
      const { req } = initialProps
      const url = req && req.url;
      let props = { url };

      const getInitialProps = PageComponent.getInitialProps;

      if (getInitialProps) {
        props = {
          ...props,
          ...getInitialProps(initialProps)
        }
      }

      return props;
    }

    public render() {
      const url = this.props.url;

      return (
        <urlContext.Provider value={url}>
          <PageComponent/>
        </urlContext.Provider>
      );
    }
  }
}

// pages/index.js
import page from 'lib/page.js'

export default page(() => {
    return (
        <div>A page</div>
    )
})

@mtford90 What does the urlContext.js file contain?

@hazae41
Copy link

hazae41 commented Jun 18, 2021

It's still happening in Next.js 11...

@marktellez
Copy link

I can confirm I am seeing this exact problem in both v10 and v11.

app.jsx wraps the pages with their contexts:

<UserProvider>
      <SessionProvider>
        <CartProvider>
          <Component {...pageProps} />
        </CartProvider>
      </SessionProvider>
    </UserProvider>

The ages get their contexts loaded just fine via client rendering/navigation with useRouter.

If there is a SSRedirect or a user enters a url directly, the state that is supposed to be available from the context is "NULL".

@timneutkens
Copy link
Member

@hazae41 @markhaslam please create a new issue with a complete reproduction so that it can be investigated. The initially reported issue has tests in the Next.js test suite.

@vercel vercel locked as resolved and limited conversation to collaborators Sep 27, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
kind: bug Confirmed bug that is on the backlog
Projects
None yet
Development

Successfully merging a pull request may close this issue.