next.js & material-ui - getting them to work
Asked Answered
F

3

3

I'm giving next.js a spin and I can't get the simplest setup to work.

Here's my setup:

Relevant libs:

  • "react": "^16.2.0",
  • "react-dom": "^16.2.0",
  • "next": "^4.2.2",
  • "express": "^4.16.2",
  • "next-routes": "^1.2.0",
  • "material-ui": "^0.20.0",

server.js

const express = require('express')
const next = require('next');
const routes = require('./routes');
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handler = routes.getRequestHandler(app, ({req, res, route, query}) => {
  render(req, res, route.page, query);
});


const server = express();

app.prepare()
.then(() => {
  server.use(handler).listen(port, (err) => {
    if (err) throw err
    console.log(`> Ready on http://localhost:${port}`)
  })
})

routes.js

const routes = module.exports = require('next-routes')();

routes
.add({name: 'walk', pattern: '/walk/:id'}) 

_document.js

import Document, { Head, Main, NextScript } from 'next/document';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';


export default class extends Document {
    static async getInitialProps(ctx) {
        const props = await Document.getInitialProps(ctx);
        const userAgent = ctx.req.headers['user-agent'];

        return {
            ...props,
            userAgent,
        };
    }

    render() {
        return (
            <html>
         <Head>
          <title>ShareWalks</title>
          <meta name="viewport" content="initial-scale=1.0, width=device-width" />
          <link rel="stylesheet" href="https://sharewalks.com/shared.css" />
         </Head>
                <body>
                <MuiThemeProvider muiTheme={getMuiTheme({ userAgent: this.props.userAgent })}>
                    <div>
                        <Main />
                        <NextScript />
                    </div>
                </MuiThemeProvider>
                </body>
            </html>
        );
    }
}

pages/index.js (this works)

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


class App extends Component {

  static async getInitialProps(args) {
    return {};
  }


  render() {
    return (
      <div>
         <Head>
          <title>ShareWalks</title>
         </Head>
         <p>Yup</p>
      </div>
    );
  }
}
export default App;

pages/walk.js (it errors here)

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


class Walk extends Component {
  static async getInitialProps({query}) {
    console.log('query: ', query);
    return {id: query.id}; //added to props
  }
  render() {
    return (
     <div>
       <Head>
        <title>Walking</title>
       </Head>
      <p>{`Walk #${this.props.id}`}</p>
       </div>
   );
  }

}

export default Walk;

When I go to localhost:8080/walk/2 or localhost:8080/walk?id=2 I get the error. The console does print out the id as expected, but then this:

query:  { id: '2' }
TypeError: Cannot read property 'toLowerCase' of undefined
    at a.renderDOM (/home/terry/myProjects/PWA/sw-next/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:36:390)
    at a.render (/home/terry/myProjects/PWA/sw-next/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:36:228)
    at a.read (/home/terry/myProjects/PWA/sw-next/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:35:250)
    at renderToString (/home/terry/myProjects/PWA/sw-next/node_modules/react-dom/cjs/react-dom-server.node.production.min.js:44:6)
    at renderPage (/home/terry/myProjects/PWA/sw-next/node_modules/next/dist/server/render.js:174:26)
    at Function.getInitialProps (/home/terry/myProjects/PWA/sw-next/node_modules/next/dist/server/document.js:83:25)
    at Function._callee$ (/home/terry/myProjects/PWA/sw-next/.next/dist/pages/_document.js:138:59)
    at tryCatch (/home/terry/myProjects/PWA/sw-next/node_modules/regenerator-runtime/runtime.js:62:40)
    at Generator.invoke [as _invoke] (/home/terry/myProjects/PWA/sw-next/node_modules/regenerator-runtime/runtime.js:296:22)
    at Generator.prototype.(anonymous function) [as next] (/home/terry/myProjects/PWA/sw-next/node_modules/regenerator-runtime/runtime.js:114:21)
Freemon answered 15/1, 2018 at 23:9 Comment(0)
F
0

OK, getting material-ui to work with Next.js is for professionals. As far as I can tell, you can't use _document.js to set up material-ui. Can someone tell me why? Anyway, I wrote a higher order component thusly:

/components/hocs/withMui.js

import React, {Component} from 'react';
import getMuiTheme from 'material-ui/styles/getMuiTheme'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import injectTapEventPlugin from 'react-tap-event-plugin' //still needed?
import myTheme from 'styles/theme'

const muiTheme = myTheme;

export default function(NextPage) {
 class outputComponent extends Component {
    static async getInitialProps(ctx) {
      const {req} = ctx;
      const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
      let pageProps = {};
      if (NextPage.getInitialProps) {
        pageProps = await NextPage.getInitialProps(ctx);
      }

      return {
        ...pageProps,
        userAgent
      }

    }
    render() {
      let userAgent = this.props.userAgent;
      return (
         <MuiThemeProvider muiTheme={getMuiTheme({userAgent,  ...muiTheme})}>
            <NextPage {...this.props} />
         </MuiThemeProvider>
      );
    }
 }

 return outputComponent;

}

and to use it with any page:

/pages/index.js

import React, {Component} from 'react'
import withMui from 'components/hocs/withMui';
import RaisedButton from 'material-ui/RaisedButton'
import Dialog from 'material-ui/Dialog'

import FlatButton from 'material-ui/FlatButton'

const styles = {
  container: {
    textAlign: 'center',
    paddingTop: 200
  }
}


class Index extends Component {
  static getInitialProps ({ req }) {

  }

  constructor (props, context) {
    super(props, context)

    this.state = {
      open: false
    }
  }

  handleRequestClose = () => {
    this.setState({
      open: false
    })
  }

  handleTouchTap = () => {
    this.setState({
      open: true
    })
  }

  render () {


    const standardActions = (
      <FlatButton
        label='Ok'
        primary={Boolean(true)}
        onClick={this.handleRequestClose}
      />
    )

    return (
        <div style={styles.container}>
          <Dialog
            open={this.state.open}
            title='Super Secret Password'
            actions={standardActions}
            onRequestClose={this.handleRequestClose}
          >
            1-2-3-4-5
          </Dialog>
          <h1>Material-UI</h1>
          <h2>example project</h2>
          <RaisedButton
            label='Super Secret Password'
            secondary
            onClick={this.handleTouchTap}
          />
        </div>
    )
  }
}

export default withMui(Index);  //<--- just wrap the page
Freemon answered 16/1, 2018 at 23:36 Comment(1)
the fix you indicated has been deprecated, in terms of supported functions. I'm curious if your fix would have solved this issue (#56316302) and if you have a new updated fix for MUI4.0.0?Shoe
C
0

First guess is to try this with your server file - the biggest issue is that you aren't setting the id param on the route itself, so on the client side - it doesn't know to look for ID.

try this:

const express = require('express')
const next = require('next')

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
  const server = express()

  server.get('/walk/:id', (req, res) => {
    return app.render(req, res, '/walk', { id: req.params.id })
  })

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(port, (err) => {
    if (err) throw err
    console.log(`> Ready on http://localhost:${port}`)
  })
})
Cathrin answered 16/1, 2018 at 5:12 Comment(2)
Hi Spencer - thanks for the help. I tried what you suggested but it still doesn't work. Could it have something to do with material-ui? I tried another experiment, I added a material-ui element (RaisedButton) on the index.js page. The error now says: TypeError: Cannot read 'prepareStyles' of undefined. Some object is not being set in time.Freemon
yea I would first strip out all third party stuff and try the above again to make sure routing is working properly.Cathrin
F
0

OK, getting material-ui to work with Next.js is for professionals. As far as I can tell, you can't use _document.js to set up material-ui. Can someone tell me why? Anyway, I wrote a higher order component thusly:

/components/hocs/withMui.js

import React, {Component} from 'react';
import getMuiTheme from 'material-ui/styles/getMuiTheme'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import injectTapEventPlugin from 'react-tap-event-plugin' //still needed?
import myTheme from 'styles/theme'

const muiTheme = myTheme;

export default function(NextPage) {
 class outputComponent extends Component {
    static async getInitialProps(ctx) {
      const {req} = ctx;
      const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
      let pageProps = {};
      if (NextPage.getInitialProps) {
        pageProps = await NextPage.getInitialProps(ctx);
      }

      return {
        ...pageProps,
        userAgent
      }

    }
    render() {
      let userAgent = this.props.userAgent;
      return (
         <MuiThemeProvider muiTheme={getMuiTheme({userAgent,  ...muiTheme})}>
            <NextPage {...this.props} />
         </MuiThemeProvider>
      );
    }
 }

 return outputComponent;

}

and to use it with any page:

/pages/index.js

import React, {Component} from 'react'
import withMui from 'components/hocs/withMui';
import RaisedButton from 'material-ui/RaisedButton'
import Dialog from 'material-ui/Dialog'

import FlatButton from 'material-ui/FlatButton'

const styles = {
  container: {
    textAlign: 'center',
    paddingTop: 200
  }
}


class Index extends Component {
  static getInitialProps ({ req }) {

  }

  constructor (props, context) {
    super(props, context)

    this.state = {
      open: false
    }
  }

  handleRequestClose = () => {
    this.setState({
      open: false
    })
  }

  handleTouchTap = () => {
    this.setState({
      open: true
    })
  }

  render () {


    const standardActions = (
      <FlatButton
        label='Ok'
        primary={Boolean(true)}
        onClick={this.handleRequestClose}
      />
    )

    return (
        <div style={styles.container}>
          <Dialog
            open={this.state.open}
            title='Super Secret Password'
            actions={standardActions}
            onRequestClose={this.handleRequestClose}
          >
            1-2-3-4-5
          </Dialog>
          <h1>Material-UI</h1>
          <h2>example project</h2>
          <RaisedButton
            label='Super Secret Password'
            secondary
            onClick={this.handleTouchTap}
          />
        </div>
    )
  }
}

export default withMui(Index);  //<--- just wrap the page
Freemon answered 16/1, 2018 at 23:36 Comment(1)
the fix you indicated has been deprecated, in terms of supported functions. I'm curious if your fix would have solved this issue (#56316302) and if you have a new updated fix for MUI4.0.0?Shoe
P
0

i have created github repository for you can use nextjs with material UI https://github.com/hadnazzar/nextjs-materialui

Tutorial for adding material ui packages and how to create example components are described here

If you are experiencing flickering issues with older versions of material UI i suggest you to look at this article

yarn add @material-ui/core

_document.js

/* eslint-disable react/jsx-filename-extension */
import React from 'react';
import Document, {
  Html, Main, NextScript,
} from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {

  // Render app and page and get the context of the page with collected side effects.
  const sheets = new ServerStyleSheets();
  const originalRenderPage = ctx.renderPage;

  ctx.renderPage = () => originalRenderPage({
    enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
  });

  const initialProps = await Document.getInitialProps(ctx);

  return {
    ...initialProps,
    // Styles fragment is rendered after the app and page rendering finish.
    styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
  };
};

_app.js

import React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import theme from '../src/theme';

export default function MyApp(props) {
  const { Component, pageProps } = props;

  React.useEffect(() => {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector('#jss-server-side');
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles);
    }
  }, []);

  return (
    <React.Fragment>
      <Head>
        <title>My page</title>
        <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </React.Fragment>
  );
}

MyApp.propTypes = {
  Component: PropTypes.elementType.isRequired,
  pageProps: PropTypes.object.isRequired,
};
Pol answered 21/5, 2022 at 23:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.