dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

React(v15)&ReduxでMaterial-UIを使ってみる

メモです。
基本的な使い方を試してみた。


リポジトリ

Redux の basic チュートリアル にそって作った Todo アプリをベースに、Material-UI を適用してみる。

https://github.com/zaki-yama/redux-todo-app/pull/2

(Material-UI 適用前のコードには base というタグをつけてます)

なお、公式 example リポジトリも存在するよう。

https://github.com/callemall/material-ui-webpack-example


インストール

$ npm install --save material-ui

でインストール。


導入手順

react-tap-event-plugin をインストールする

http://www.material-ui.com/#/get-started/installation によると、react-tap-event-plugin というパッケージが必要らしい。
Material-UI と同様に npm でインストールする。

$ npm install --save react-tap-event-plugin

このパッケージにより、Material-UI のすべてのコンポーネントは onTouchTap というイベントハンドラが使えるようになる。

マウスクリックやスマホタブレットのタッチ・タップイベントを共通のイベントハンドラで処理できるようになる、のかな?

また、上記ページには

This dependency is temporary and will go away once the official React version is released.

と書いており、試したときの React のバージョンは 15.3.2 だったので
メジャーバージョンが 1 以上だからこのプラグイン無しでも動くということかな?と思ったけど、結局インストールしないとうまく動かなかった。

react-tap-event-plugin をインストールした後は、以下のコードをアプリの先頭に記述する必要がある。

import injectTapEventPlugin from 'react-tap-event-plugin';

// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();

よって Redux の場合、アプリのエントリーポイント(src/index.js)に以下のように追記するはず。

 import { render } from 'react-dom';
 import { Provider } from 'react-redux';
 import { createStore } from 'redux';
+import injectTapEventPlugin from 'react-tap-event-plugin';
 import todoApp from './reducers';
 import App from './containers/App';
  
+// Needed for onTouchTap
+// http://stackoverflow.com/a/34015469/988941
+injectTapEventPlugin();
+
 const store = createStore(todoApp);
  
 render(
   <Provider store={store}>
     <App />
   </Provider>,
   document.getElementById('root'),
 );


Roboto フォントを適用する

またまた
http://www.material-ui.com/#/get-started/installation#roboto-font
を読むと、

Material-UI was designed with the Roboto font in mind

とあるので、Roboto フォント を読み込むようにする。

html ファイルに

<!-- src/index.html -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">

と書き、CSS でも

/* src/main.css */
html {
  font-family: 'Roboto', sans-serif;
}

とする。
確認したところ html 側の記述だけでも Material-UI コンポーネントに Roboto フォントが適用されるが、
アプリ全体のフォントを統一するため CSS でも指定することにした。

なお、話は逸れるが webpack で CSS を扱う方法はいくつか考えられる。

├── dist .................. ビルド後のファイルを置く場所
├── src
│   ├── actions
│   ├── components
│   ├── constants
│   ├── containers
│   ├── index.html
│   ├── index.js
│   ├── main.css
│   └── reducers
└── webpack.config.js

というディレクトリ構成の場合、

1) CSSCSS のままで html から読み込む場合

html 側は

<link rel="stylesheet" type="text/css" href="main.css">

とし、webpack.config.js では file-loader を使って dist ディレクトリに CSS ファイルもコピーされるようにする。

   context: __dirname + '/src',
   entry: {
     javascript: './index.js',
-    html: './index.html'
+    html: './index.html',
+    css: './main.css',
   },
   output: {
     path: __dirname + '/dist',
@@ -27,6 +28,10 @@ module.exports = {
       {
         test: /\.html$/,
         loader: 'file?name=[path][name].[ext]'
+      },
+      {
+        test: /\.css$/,
+        loader: 'file?name=[path][name].[ext]'
       }
     ]
   }

2) JavaScript にバンドルする場合

CSSJavaScript 内で import してしまうという場合、html の代わりに src/index.js で

import './main.css';

とし、webpack.config.js では css-loader と style-loader を使うようにする。

       {
         test: /\.html$/,
         loader: 'file?name=[path][name].[ext]'
-      }
+      },
+      {
+        test: /\.css$/,
+        loader: 'style!css',
+      },
     ]

どちらでもいいですが、2) の方が後々 SCSS とかにも応用できるので良さげ。


<MuiThemeProvider> でアプリケーション全体をラップする

コンポーネントを使う前の最後の準備として、アプリケーション全体を <MuiThemeProvider> というコンポーネントでラップする。
エントリーポイントになっている src/index.js でやってもいいと思うし、コンポーネントの root になっている containers/App.js でもいいはず。

containers/App.js

 import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+
 import AddTodo from './AddTodo';
 import VisibleTodoList from './VisibleTodoList';
 import FilterLinkList from '../components/FilterLinkList';

 export default class App extends React.Component {
   render() {
     return (
-      <div>
-        <AddTodo />
-        <VisibleTodoList />
-        <FilterLinkList />
-      </div>
+      <MuiThemeProvider>
+        <div>
+          <AddTodo />
+          <VisibleTodoList />
+          <FilterLinkList />
+        </div>
+      </MuiThemeProvider>
     );
   }
 }


Material-UI コンポーネントを使う

ここまでできるとようやく Material-UI のコンポーネントを使うことができる。

今回は簡単な例で、Todo の入力フォームとボタンをそれぞれ TextFieldRaisedButton に置き換えてみる。

元のコードは以下。

components/AddTodoForm.js

import React from 'react';

export default class AddTodoForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    const node = this.refs.input;
    const text = node.value.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    node.value = '';
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <input ref="input" />
          <button type="submit">
            Add Todo
          </button>
        </form>
      </div>
    );
  }
}

これを以下のようにする。

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

export default class AddTodoForm extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: '',
    };
  }

  handleChange(e) {
    this.setState({
      input: e.target.value,
    });
  }

  handleSubmit(e) {
    e.preventDefault();
    const text = this.state.input.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    this.setState({
      input: '',
    });
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <TextField
            id="todo-title"
            value={this.state.input}
            onChange={(e) => this.handleChange(e)}
          />
          <RaisedButton
            label="Add Todo"
            onTouchTap={(e) => this.handleSubmit(e)}
          />
        </form>
      </div>
    );
  }
}

フォームに入力されたテキストを管理するのに ref を使っていたのを local state で管理するようになったため全体のコード量が増えた。
ref のまま Material-UI のコンポーネントを管理することもできるみたいなんだけど、ドキュメントに載っていない方法なのでやめた。
あとで別の記事にでもメモする。

この変更によってアプリの UI は以下のように変更される。

before

f:id:dackdive:20161116014935p:plain

after

f:id:dackdive:20161116014944p:plain

ちょっとだけそれっぽい UI になった。


おわりに

ということで React/Redux なアプリに Material-UI を導入することができた。
意外と事前準備としてやることが多く、またそれがドキュメントからは微妙にわかりづらかったりして思った以上に手こずった。


トラブルシューティング的な

ボタンを使うと Unknown prop `onTouchTap` on <button> tag. が出る

→react-tap-event-plugin が入っていない。


リファレンス