Next.js: Context does not apply through <Main />

Created on 24 Jan 2017  Â·  13Comments  Â·  Source: vercel/next.js

Hello,

I'm currently using material-ui in my app. I'm taking advantage of pages/_document.js, along with <Main />:

// pages/_document.js

render() {
    return (
 <html>
        <Head>
        </Head>
        <body>
            <MuiThemeProvider muiTheme={{ ... ]}>
              <Main />
            </MuiThemeProvider>
          <NextScript />
        </body>
      </html>
    );
}

Then the page being loaded:

// pages/index.js
render() {
    console.log(this.context.muiTheme) // undefined
    return (...);
}
...
Index.contextTypes = {
    muiTheme: React.PropTypes.object.isRequired
};

So normally, the context would be passed through and I'd have access to the theme properties, and so would any material-ui components. However, in this case the context is not being applied.

Logging out the context in the Main source code works.

1) Is this expected?
2) Is there any work around?

Most helpful comment

For those interested, here's the solution we took, handles both Redux & Material-UI:

// pages/index.js
function Index() {
    ...
}

export default Page(connect()(Index));

Every page is wrapped in a Page decorator/HOC, which does the following:

const decorator = (ComposedComponent) => {
  return class extends Component {

    static async getInitialProps(ctx) {
      const { req } = ctx;
      const isServer = !!req;
      const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;

      // Second param here is initial redux state on the server
      const store = initStore(reducers, {}, isServer);

      let pageProps = {};

      if (ComposedComponent.getInitialProps) {
        pageProps = await ComposedComponent.getInitialProps(ctx);
      }

      return {
        ...pageProps,
        initialState: store.getState(),
        isServer,
        userAgent,
      };
    }

    constructor(props) {
      super(props);
      this.store = initStore(reducers, props.initialState, props.isServer)
    }

    render() {
      return (
        <div>
          <Provider store={this.store}>
            <MuiThemeProvider muiTheme={getMuiTheme({ userAgent: this.props.userAgent })}>
              <ComposedComponent
                {...this.props}
              />
            </MuiThemeProvider>
          </Provider>
        </div>
      )
    }
  };
};

export default decorator;

This now provides Redux & MUI throughout every page. It also allows the actual page file to use getInitialProps as well, using the following lines in the decorator:

      if (ComposedComponent.getInitialProps) {
        pageProps = await ComposedComponent.getInitialProps(ctx);
      }

These are then applied into the props of the contained page/component.

All 13 comments

@Ehesp can I have a sample repo. So, I could work on this pretty fast.

Sure, should hopefully be able to slap something up in the next couple of
hours.

On 24 Jan 2017 7:53 pm, "Arunoda Susiripala" notifications@github.com
wrote:

@Ehesp https://github.com/Ehesp can I have a sample repo. So, I could
work on this pretty fast.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/zeit/next.js/issues/873#issuecomment-274918824, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAzZXiaxLrsVrtxGgaZyea1Z3e7EK3pJks5rVlacgaJpZM4Lsscv
.

@arunoda Here you go: https://github.com/Ehesp/next-mui-context

Install modules and just npm run dev. So basically the _default.js file wraps the entire application in MuiThemeProvider. Under the hood this just provides context to the app. The MUI component within index.js (<RaisedButton ...>) then uses this context to apply the theme. Currently it errors with TypeError: Cannot read property 'prepareStyles' of undefined, which means there's no context (https://github.com/callemall/material-ui/issues/5330#issuecomment-251843011).

In Index, if you comment out render and uncomment the currently commented ones, it works.

@Ehesp hey, I checked it and unfortunately that's something you can't do.
The idea behind the _document.js is to customize the base HTML document of the app.
Although it uses React, it's something only runs on the server. So, React is just an API to customize the base HTML page.

Actual, Next.js app is runs inside the <Main/>. So, basically that's where the react's root element lives.

So, you need to provide your MUI context in the page's render method.

Also there's a problem with the SSR when doing styles. Check the console.
That's because of the userAgent mismatch in the server and client.

Here's a easy and ugly fix: https://github.com/callemall/material-ui/pull/3009#issuecomment-177713403

But here's the proper way to do it:

import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';

import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';

export default class extends React.Component {

    static getInitialProps = ({ req }) => {
        const userAgent = req? req.headers['user-agent'] : navigator.userAgent
        return { userAgent }
    }

    render() {
        const { userAgent } = this.props
        return (
            <div>
                <MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme, { userAgent })}>
                    <div>
                        <p>Render a styled MUI Button: </p>
                        <RaisedButton label="Button" primary style={{ margin: 12 }} />
                    </div>
                </MuiThemeProvider>
            </div>
        );
    }
}

I use material-ui in my app. I created a layout component that will just wrap whatever is passed into the layout component with the theme provider. Is that what your trying to accomplish?

Hey. Thanks for checking that out.

That's fine then, I'll make a decorator to contain each of the pages. I'm
assuming this is going to be the same as Redux then using Provide.

Cheers

On 25 Jan 2017 3:25 am, "Tyler Knipfer" notifications@github.com wrote:

I use material-ui in my app. I created a layout component that will just
wrap whatever is passed into the layout component. Is that what your trying
to accomplish?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/zeit/next.js/issues/873#issuecomment-275009300, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAzZXjsgrkktK37JGYjglSTNAbMW4T9Pks5rVsCkgaJpZM4Lsscv
.

@arunoda https://github.com/zeit/next.js/issues/590

I believe this is a similar issue. If wrapping each page item in Provider, which provides store context is rerendered on each page load how is state supposed to be persisted? Each page would have a fresh store.

It's almost as if there needs to be a React wrapper for every page?

For those interested, here's the solution we took, handles both Redux & Material-UI:

// pages/index.js
function Index() {
    ...
}

export default Page(connect()(Index));

Every page is wrapped in a Page decorator/HOC, which does the following:

const decorator = (ComposedComponent) => {
  return class extends Component {

    static async getInitialProps(ctx) {
      const { req } = ctx;
      const isServer = !!req;
      const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;

      // Second param here is initial redux state on the server
      const store = initStore(reducers, {}, isServer);

      let pageProps = {};

      if (ComposedComponent.getInitialProps) {
        pageProps = await ComposedComponent.getInitialProps(ctx);
      }

      return {
        ...pageProps,
        initialState: store.getState(),
        isServer,
        userAgent,
      };
    }

    constructor(props) {
      super(props);
      this.store = initStore(reducers, props.initialState, props.isServer)
    }

    render() {
      return (
        <div>
          <Provider store={this.store}>
            <MuiThemeProvider muiTheme={getMuiTheme({ userAgent: this.props.userAgent })}>
              <ComposedComponent
                {...this.props}
              />
            </MuiThemeProvider>
          </Provider>
        </div>
      )
    }
  };
};

export default decorator;

This now provides Redux & MUI throughout every page. It also allows the actual page file to use getInitialProps as well, using the following lines in the decorator:

      if (ComposedComponent.getInitialProps) {
        pageProps = await ComposedComponent.getInitialProps(ctx);
      }

These are then applied into the props of the contained page/component.

@arunoda should we create an example showing how to use material-ui with next.js? We've had questions about material-ui multiple times 😄

@timneutkens why not :)

Hey @arunoda, really like the library but this is a main issue in my opinion.
It looks like there is currently no way for a component to live globally in the app ?
Would be nice to have a place to declare components outside the pages.

A solution that I just wrote and havent fully tested out but seems to be working is

import { compose } from 'redux';
import withRedux from 'next-redux-wrapper';
import store from '../../store/index';
import { userAgent, withMuiTheme } from './withMUITheme';

// compose higher order Component
export default compose(withMuiTheme(userAgent), withRedux(store));

```withMuiTheme
import React from 'react';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

export const userAgent = getMuiTheme({ userAgent: false });
export const withMuiTheme = userAgent => Component => () =>




;

then my page can be exported

import withMUITheme from '../MaterialHOC/index';
const Board = () =>






;

export default withMUITheme(Board);
````

Was this page helpful?
0 / 5 - 0 ratings

Related issues

renatorib picture renatorib  Â·  3Comments

wagerfield picture wagerfield  Â·  3Comments

lixiaoyan picture lixiaoyan  Â·  3Comments

rauchg picture rauchg  Â·  3Comments

olifante picture olifante  Â·  3Comments