react + redux + next + material ui + typescript

はじめに

友人との開発でreact + redux + next + typescript + material uiを使用したかったのですが、該当しそうなサンプルがなかったので作成しました。

まだ作り込んでいないこともありますがredux typescriptに関しては、ベストプラクティスが正直あんまりわかっていないので、何かあればぜひコメントをいただきたいです!

それでは、作ったリポジトリをもとに解説していきたいと思います!
https://numanuma.net/?p=50&preview=true

使用ライブラリ

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
  "name": "next-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@material-ui/core": "^3.0.2",
    "@types/next": "^6.1.2",
    "@types/next-redux-wrapper": "^2.0.0",
    "@types/node": "^10.9.4",
    "@types/react": "^16.4.11",
    "@types/react-redux": "^6.0.6",
    "@zeit/next-typescript": "^1.1.0",
    "babel-plugin-module-resolver": "^3.1.1",
    "jss": "^9.8.7",
    "module-alias": "^2.1.0",
    "next": "^6.1.1",
    "next-redux-wrapper": "^2.0.0",
    "react": "^16.4.2",
    "react-dom": "^16.4.2",
    "react-redux": "^5.0.7",
    "redux": "^4.0.0",
    "typescript": "^3.0.1"
  },
  "devDependencies": {
    "fork-ts-checker-webpack-plugin": "^0.4.9",
    "redux-devtools-extension": "^2.13.5"
  },
  "_moduleAliases": {
    "~": "src"
  }
}

_moduleAliasesはtypecheckの際にaliasでエラーが出るのを防ぐ

next.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const path = require('path')

module.exports = withTypescript({
  webpack(config, options) {
    config.resolve.alias = {
      '~': './src'
    }

    if (options.isServer) {
      config.plugins.push(new ForkTsCheckerWebpackPlugin())
    }
   
    return config
  }
})

fork-ts-checker-webpack-pluginはファイルの変更がかかった際にtypecheckをかけるもの。

_app.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App, { Container } from 'next/app'
import withRedux from 'next-redux-wrapper'
import { devToolsEnhancer } from 'redux-devtools-extension'
import { SheetsRegistry } from 'jss';
import { createMuiTheme, createGenerateClassName, MuiThemeProvider } from '@material-ui/core/styles';
import purple from '@material-ui/core/colors/purple';
import green from '@material-ui/core/colors/green';
import JssProvider from 'react-jss/lib/JssProvider';
import { CssBaseline } from '@material-ui/core';

// redux ===============================================
const reducer = (state = {foo: ''}, action) => {
  switch (action.type) {
    case 'FOO':
      return {...state, foo: action.payload};
    default:
      return state
  }
};

const makeStore = (initialState, _) => {
  return createStore(
    reducer,
    initialState,
    devToolsEnhancer({})
  )
}
// =====================================================

// material ui =========================================
const theme = createMuiTheme({
  palette: {
    primary: {
      light: purple[300],
      main: purple[500],
      dark: purple[700]
    },
    secondary: {
      light: green[300],
      main: green[500],
      dark: green[700]
    }
  }
})

function createPageContext() {
  return {
    theme,
    sheetsManager: new Map(),
    sheetsRegistry: new SheetsRegistry(),
    generateClassName: createGenerateClassName(),
  }
}

function getPageContext() {
  if (!process.browser) {
    return createPageContext()
  }

  if (!global.__INIT_MATERIAL_UI__) {
    global.__INIT_MATERIAL_UI__ = createPageContext()
  }

  return global.__INIT_MATERIAL_UI__
}
// =====================================================
interface AppProps {
  Component: React.Component
  pageProps: any
  store: any
}

class MyApp extends App<AppProps> {
  pageContext = null

  constructor(props) {
    super(props)

    this.pageContext = getPageContext()
  }
 
  static async getInitialProps({ Component, ctx }) {
    ctx.store.dispatch({type: 'FOO', payload: 'foo'})

    const pageProps =
      Component.getInitialProps ? await Component.getInitialProps(ctx) : {}

    return { pageProps }
  }

  componentDidMount() {
    // SSR時のCSSを削除する
    console.log(document.querySelector('#jss-server-side'))
    const jssStyles = document.querySelector('#jss-server-side')

    if (jssStyles && jssStyles.parentNode) {
      jssStyles.parentNode.removeChild(jssStyles)
    }
  }

  render() {
    const { Component, pageProps, store } = this.props

    return (
      <Container>
        <JssProvider
          registry={ this.pageContext.sheetsRegistry }
          generateClassName={ this.pageContext.generateClassName }
        >
          <MuiThemeProvider
            theme={ this.pageContext.theme }
            sheetsManager={ this.pageContext.sheetsManager }
          >
            <CssBaseline />
            <Provider store={ store }>
              <Component pageContext={ this.pageContext } { ...pageProps } />
            </Provider>              
          </MuiThemeProvider>
        </JssProvider>
      </Container>
    )
  }
}

export default withRedux(makeStore)(MyApp)

_app.tsxはnext.jsで決められたルールで、全てのページで共通で使用される、componentの親componentとして使用されるものです。
react componentを必ずexportする。

ここでレンダリングするcomponentに対して、material uiやreduxで生成したpropなどを、ページごとに使用してあげるように設定していきます。

34行目のthemeという変数は、material uiで使用する色をまとめて定義します。

58行目のgetPageContext関数は、サーバーサイドレンダリングする際に
material uiが参照する設定をglobalに追加してあげる関数です。

_document.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import React from 'react'
import Document, { Head, Main, NextScript } from 'next/document'
import flush from 'styled-jsx/server'

class MyDocument extends Document {
  static getInitialProps(context) {
    let pageContext;
   
    const page = context.renderPage(Component => {
      const WrappedComponent = props => {
        pageContext = props.pageContext;
        return <Component {...props} />;
      };

      return WrappedComponent;
    });

    return {
      ...page,
      pageContext,
      styles: (
        <React.Fragment>
          <style
            id="jss-server-side"
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: pageContext.sheetsRegistry.toString() }}
          />
          {flush() || null}
        </React.Fragment>
      ),
    };
  }

  render() {
    const { pageContext } = this.props

    return (
      <html>
        <Head>
          <title>My Page</title>
          <meta charSet="utf-8"/>
          <meta
            name="viewport"
            content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
          />
          <meta name="theme-color" content={pageContext.theme.palette.primary.main} />
          <link
            rel="stylesheet"
            href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

export default MyDocument

_document.tsxはアプリケーション全体のhtmlにデフォルトを与えてあげるものです。共通のCSSを当ててあげたり、メタタグを入れるのに使用します。

ここではサーバーサイドレンダリング時に当てる用のスタイル(flush() css-in-jsをサーバーサイドレンダリングに対応させるため)とフォントを入れています。

全然詳しい説明省いちゃいましたが、とりあえず動くので
githubを良かったら見てみてください!

さいごに

記事書いている途中でほかにもやることあっておろそかになってしまいましたが
今日はこの辺にしておきたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です