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'
が必要ないのは少しきもいです。
export default () => <div>これから始めたNext.js</div>;
起動スクリプトは以下としました。
"scripts": {
"start": "next"
},
$ npm run start
でlocalhost:3000にnext.jsアプリが起動します。この時点でホットリロードも効いています。
後述にある_document.js
などを新しく追加した場合にはアプリのリロードが必要です。変更したけど反映されないなどの場合は、一度アプリを起動し直してみると変なところでつまずかずに済むと思います。
ビルドとproductionサーバー
ビルドしての本番運用がつらいとNext.js
でSSRする意味が薄いので先に試してみます。
"scripts": {
"start": "next",
"build": "next build",
"production": "next start"
},
next build
でビルドします。next start
でbuildされた.next
からサーバーを起動してくれます。pm2による永続起動は以下です。
"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に追加することで解決できます。
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}
これでエラーは解決されますが、初回のロード時にスタイルが遅れてでるよ。状態になるので_document.js
を追加します。公式のexample 通りです。
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
はできる限り手を入れない方向で進めています。
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を参考にしました。
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);
import { combineReducers, createStore } from 'redux';
import { message } from './message';
const rootReducer = combineReducers({ message });
export default initialState => createStore(rootReducer, initialState);
recompose
NextJSの拡張であるgetInitialProps
をrecompose
で扱います。
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に渡します。
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
で作ることができます。
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?