LoginSignup
13
13

More than 5 years have passed since last update.

これから始めたNext.js

Last updated at Posted at 2019-01-27

Next.jsを触り始めたのでその過程を残しておこうと思います。書いたコードはgithubに上げてあります(natsuhikok/next-react)。Next.jsのexampleが充実してて迷わずに進めるところが好きでした。

インストールして動かしてみます

インストールとpages/index.jsだけで動くのでcreate-react-appするより、らくちんかもしれません。

$ npm install --save next react react-dom

import React from 'react'が必要ないのは少しきもいです。

pages/index.js
export default () => <div>これから始めたNext.js</div>;

起動スクリプトは以下としました。

package.json
"scripts": {
   "start": "next"
 },

$ npm run startでlocalhost:3000にnext.jsアプリが起動します。この時点でホットリロードも効いています。

後述にある_document.jsなどを新しく追加した場合にはアプリのリロードが必要です。変更したけど反映されないなどの場合は、一度アプリを起動し直してみると変なところでつまずかずに済むと思います。

ビルドとproductionサーバー

ビルドしての本番運用がつらいとNext.jsでSSRする意味が薄いので先に試してみます。

package.json
"scripts": {
  "start": "next",
  "build": "next build",
  "production": "next start"
},

next buildでビルドします。next startでbuildされた.nextからサーバーを起動してくれます。pm2による永続起動は以下です。

package.json
"scripts": {
    "start": "next",
    "build": "next build",
    "production": "next start",
    "pm2": "next build && pm2 start npm --name 'next' -- run production"
}

これで安心して他の部分を試していけます。

styled-components

NextJSでstyled-componentsを試したところ特定の条件下で以下のエラーがでてしまいました。

Expected server HTML to contain a matching <tag> in <tag>...

ExpressなどでReactをSSRしたときにも見かけるあれです。これはbabel-plugin-styled-componentsをbabelに追加することで解決できます。

 .babelrc
{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

これでエラーは解決されますが、初回のロード時にスタイルが遅れてでるよ。状態になるので_document.jsを追加します。公式のexample 通りです。

_document.js
import Document from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps (ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
        });
      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: [...initialProps.styles, ...sheet.getStyleElement()],
      };
    } finally {
      sheet.seal();
    }
  }
}

createGlobalStyleはどこでするべきか

Metaデータを納めるコンポーネントを作って各ページで読み込むのがよさそうです。_app.jsで読み込むこともできそうですが_document.js, _app.jsはできる限り手を入れない方向で進めています。

components/Meta.js
import Head from 'next/head';

export default ({ title }) => (
  <>
    <Head>
      <title>{title}</title>
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      <meta charSet="utf-8" />
    </Head>
    <GlobalStyle />
  </>
);

import { createGlobalStyle } from 'styled-components';
import normalize from 'styled-normalize';

const GlobalStyle = createGlobalStyle`
  ${normalize}
  * { box-sizing: border-box; }
  html {
    font-size: 62.5%;
    font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Yu Gothic", YuGothic, Verdana, Meiryo, "M+ 1p", sans-serif;
  }
  input { border-radius: 0; }
  body {
    font-size: 1.4rem;
  }
  p, h1, h2, h3, h4, h5, h6 { margin: 0; }
  ul {
    margin: 0;
    padding: 0;
    list-style-type: none;
  }
`

GlobalStyleはタイミングで別コンポーネントにすることが多いです。

redux

NextJSでReduxを使ってみます。生React+reduxとの違いは_app.jsくらいかと思います。公式のexamleを参考にしました。

_app.js
import { Provider } from 'react-redux';
import App, { Container } from 'next/app';
import withRedux from 'next-redux-wrapper';
import makeStore from '../store/makeStore';

class ExtendedApp extends App {
  static getInitialProps = async ({ Component, ctx }) => ({
    pageProps: (Component.getInitialProps ? await Component.getInitialProps(ctx) : {})
  })
  render () {
    const { Component, pageProps, store } = this.props;
    return (
      <Container>
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    );
  }
}

export default withRedux(makeStore)(ExtendedApp);

store/makeStore.js
import { combineReducers, createStore } from 'redux';
import { message } from './message';

const rootReducer = combineReducers({ message });

export default initialState => createStore(rootReducer, initialState);

recompose

NextJSの拡張であるgetInitialPropsrecomposeで扱います。

pages/index.js
import { connect } from 'react-redux';
import { compose, withState, setStatic, pure, withHandlers } from 'recompose';

import Navigation from '../components/Navigation';
import Meta from '../components/Meta';
import { updateMessage } from '../store/message';

const Index = ({ message, handleClick, inputValue, handleInputChange }) => (
  <>
    <Meta title="これから始めたNext.js" />
    <Navigation />
    <Headline>これから始めたNext.js</Headline>
    <p>{message}</p>
    <input value={inputValue} onChange={handleInputChange} />
    <button onClick={handleClick}>submit</button>
  </>
);

export default compose(
  setStatic('getInitialProps', async props => {
    const { isServer } = props;
    if(!isServer) {
      console.log(props);
    }
  }),
  withState('inputValue', 'setInputValue', ''),
  connect(
    ({ message }) => ({ message: message.message }),
    dispatch => ({
      updateMessage: value => dispatch(updateMessage(value)),
    }),
  ),
  withHandlers({
    handleClick: ({ setInputValue, updateMessage, inputValue }) => () => {
      updateMessage(inputValue);
      setInputValue('');
    },
    handleInputChange: ({ setInputValue }) => e => setInputValue(e.target.value),
  }),
  pure,
)(Index);

import styled from 'styled-components';
const Headline = styled.h1`
  font-size: 32px;
  line-height: 1.8;
  border-bottom: 4px dotted blue;
`;

Dynamic URL

user/:idのような動的なルーティングが必要です。残念ながらNextJSでこれを実現するためにはserver.jsを追加するなど自前でカスタムサーバー・ルーティングする必要があります。ここでは最小限で実装できるuser?id=idの方式を試してみます。気持ちダサいですがSEO的には問題のない方法です。

queryはgetInitialPropsから取得できるのでこれをViewに渡します。

pages/users.js
import { compose, setStatic, pure } from 'recompose';
import Navigation from '../components/Navigation';
import Meta from '../components/Meta';

const Posts = ({ id }) => (
  <div>
    <Meta title="これから始めたNext.js" />
    <Navigation />
    <h1>これは{id}番目のユーザーページです</h1>
  </div>
);

export default compose(
  setStatic('getInitialProps', async props => {
    const { query: { id } } = props;
    return { id };
  }),
  pure,
)(Posts);

queryを指定したリンクはnext/Linkで作ることができます。

components/Navigation.js
import Link from 'next/link';

export default () => (
  <ul>
    <li><Link href="/"><a>home</a></Link></li>
    <li><Link href="/about"><a>About</a></Link></li>
    <li>
      <Link href={{ pathname: '/users', query: { id: 3 } }}>
        <a>User</a>
      </Link>
    </li>
  </ul>
);

参考

zeit/next.js
Example app with styled-components
Redux example
how can I use next in pm2?

13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13