Step3: Reactコンポーネントを作ろう!

このステップではチュートリアルに必要なReactコンポーネントを作成します。

Reactコンポーネント

Reactはコンポーネントという形でUI(User Interface)とロジックを1つのJSファイルにまとめて管理します。このチュートリアルではコンポーネントを再利用しやすいように、1ファイル1コンポーネントで作成していきます。

今回作成するコンポーネントとコンポーネント間の親子関係は以下のようになります。

<AppLayout>
  <Header />
  <MemoList>
    <MemoItem />
    <MemoItem />
    <MemoItem />
    ...
  </MemoList>
</AppLayout>

それでは、各コンポーネントを一つ一つ作成していきましょう。

AppLayout コンポーネント(レイアウト)

AppLayoutは複数のコンポーネントを管理したり配置を決める役割を持つコンポーネントです。このチュートリアルではレイアウトと呼ぶことにします。

Meteorアプリディレクトリ内に、imports/ui/layouts ディレクトリを作成し、その中に AppLayout.js を作成します。

$ mkdir -p imports/ui/layouts
$ touch imports/ui/layouts/AppLayout.js

imports/ui/layouts/AppLayout.js

import React from 'react';

export default class AppLayout extends React.Component {
  render() {
    return (
      <div className="container">
        <h1>Hello, Meteor React World!</h1>
        <h2>App Layout</h2>
      </div>
    );
  }
}

そして client/main.js を以下のように書き換えます。

client/main.js

import { Meteor } from 'meteor/meteor';
import React from 'react';
import { render } from 'react-dom';
import AppLayout from '../imports/ui/layouts/AppLayout';

Meteor.startup(() => {
  render(
    <AppLayout />,
    document.getElementById('render-root')
  );
});

これでAppLayoutが読み込まれるようになりました。

ポイント

  • ES2015のclass構文を使って React.Component を継承したオブジェクトを作成する
  • export で別ファイルから import できるようにする
  • client/main.js の中で AppLayout.jsimport する
    • export default の場合は、import 時に波括弧 { } が不要
    • import 時、ファイル名の拡張子は省略可能
  • Meteor 1.3からはimportsディレクトリ内にファイルを置くことで、Meteorの自動読み込みの対象外となり、必要な時に読み込まれるようになる(遅延読み込み)

Header コンポーネント

続いてページのヘッダー部分のHeaderコンポーネントを作成します。

imports/ui/components ディレクトリを作成し、その中に Header.js ファイルを作成します。

$ mkdir -p imports/ui/components
$ touch imports/ui/components/Header.js

imports/ui/components/Header.js

import React from 'react';

export default class Header extends React.Component {
  render() {
    return (
      <header className="header">
        <h1>Simple Memo</h1>
      </header>
    );
  }
}

そして AppLayout 内で Header コンポーネントを読み込みます。

imports/ui/layouts/AppLayout.js

import React from 'react';
import Header from '../components/Header';

export default class AppLayout extends React.Component {
  render() {
    return (
      <div className="container">
        <Header />
      </div>
    );
  }
}

次のように表示されます。

MemoItem / MemoList コンポーネント

最後にメモ自身である MemoItemコンポーネントと、メモ一覧を表すMemoListコンポーネントを作成します。

それぞれ imports/ui/componets ディレクトリ内に作成していきます。

$ touch imports/ui/components/MemoItem.js
$ touch imports/ui/components/MemoList.js

imports/ui/components/MemoItem.js

import React from 'react';

export default class MemoItem extends React.Component {
  render() {
    const { memo } = this.props;
    return (
      <div className="memo-item">
        <textarea className="textarea" defaultValue={memo.content} />
      </div>
    );
  }
}

MemoItem.propTypes = {
  memo: React.PropTypes.object.isRequired,
};

imports/ui/components/MemoList.js

import React from 'react';
import MemoItem from './MemoItem';

export default class MemoList extends React.Component {
  render() {
    const { memos } = this.props;
    return (
      <div className="memo-list">
        {memos.map(memo => (
          <MemoItem key={memo._id} memo={memo} />
        ))}
      </div>
    );
  }
}

MemoList.propTypes = {
  memos: React.PropTypes.array.isRequired,
};

AppLayoutにMemoListコンポーネントを追加します。

imports/ui/layouts/AppLayout.js

import React from 'react';
import Header from '../components/Header';
import MemoList from '../components/MemoList';

export default class AppLayout extends React.Component {
  render() {
    const { memos } = this.props;
    return (
      <div className="container">
        <Header />
        <MemoList memos={memos} />
      </div>
    );
  }
}

AppLayout.propTypes = {
  memos: React.PropTypes.array.isRequired,
};

最後にAppLayoutを呼び出すときにメモデータを渡すようにします。

client/main.js

import { Meteor } from 'meteor/meteor';
import React from 'react';
import { render } from 'react-dom';
import AppLayout from '../imports/ui/layouts/AppLayout';

// sample data
const memos = [
  {_id: 'memo1', content: 'This is sample data 1'},
  {_id: 'memo2', content: 'This is sample data 2'},
  {_id: 'memo3', content: 'This is sample data 3'},
  {_id: 'memo4', content: 'This is sample data 4'},
  {_id: 'memo5', content: 'This is sample data 5'},
];

Meteor.startup(() => {
  render(
    <AppLayout memos={memos} />,
    document.getElementById('render-root')
  );
});

ポイント

  • Reactでは親コンポーネントから子コンポーネントに値を渡すことができ、子コンポーネント内でthis.propsを参照することで親から渡された値を取得できる
  • 親コンポーネントでデータを管理し、子コンポーネントに必要なデータを渡すようにすると、コンポーネントの保守性・再利用性を高めることができる
  • static get propTypes() で親コンポーネントから受け取った props のバリデーションを行っている(Reactの機能

コードを書き換えると、以下のように表示されます。

これで必要なコンポーネントが作成されました。

スタイルの適用

アプリの見た目を整えるため、スタイルを適用します。

client/main.css

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background-color: #E8E8E8;
  color: #4E4E4E;
  font-weight: 100;
  line-height: 1.4;
  font-family: "Hiragino Sans", "Hiragino Kaku Gothic ProN","メイリオ", sans-serif;
}

.container {
  padding: 10px;
}

.header {
  margin-left: 10px;
}

.header h1 {
  margin: 5px 0 10px
}

.add-button {
  margin-left: 10px;
  margin-bottom: 10px;
  font-size: 20px;
  padding: 5px 20px;
}

.memo-list {
  width: 100%;
}

.memo-item {
  display: inline-block;
  position: relative;
  background-color: #FEF3A1;
  min-height: 200px;
  width: 300px;
  border: 1px solid rgba(200, 200, 200, 0.2);
  box-shadow: 1px 1px 2px 0 rgba(0, 0, 0, 0.2);
  border-radius: 2px;
  padding: 5px 10px;
  margin: 10px;
  font-size: 20px;
}


@media screen and (max-width: 705px) {
  .memo-item {
    width: calc(100% - 45px);
  }
}

.memo-item .remove-button {
  display: none;
  position: absolute;
  top: 3px;
  right: 5px;
  font-size: 24px;
  text-decoration: none;
}

.memo-item:hover {
  box-shadow: 1px 1px 3px 3px rgba(0,0,0,0.1);
}

.memo-item:hover .remove-button {
  display: block;
}

.memo-item .textarea {
  width: 100%;
  height: 100%;
  min-height: 200px;
  border: none;
  background: none;
  overflow: inherit;
  outline: none;
  -webkit-box-shadow: none;
  -moz-box-shadow: none;
  box-shadow: none;
  resize: none;
  font: inherit;
}

.memo-item .textarea:focus {
  border: none;
}

.noselect {
  -webkit-touch-callout: none; /* iOS Safari */
  -webkit-user-select: none;   /* Chrome/Safari/Opera */
  -khtml-user-select: none;    /* Konqueror */
  -moz-user-select: none;      /* Firefox */
  -ms-user-select: none;       /* IE/Edge */
  user-select: none;           /* non-prefixed version, currently                                  not supported by any browser */
}

.memo-item.is-color-yellow { background-color: #FEF3A1; }
/*.memo-item.is-color-blue { background-color: #42afe3; }*/
.memo-item.is-color-blue { background-color: #AFF4FE; }
/*.memo-item.is-color-green { background-color: #97cd76; }*/
.memo-item.is-color-green { background-color: #B4FDA5; }
.memo-item.is-color-orange { background-color: #f68b39; }
.memo-item.is-color-red { background-color: #ed8c83; }
/*.memo-item.is-color-turquoise { background-color: #1fc8db; }*/
.memo-item.is-color-gray { background-color: #aeb1b5; }
.memo-item.is-color-pink { background-color: #FEC7C8; }
.memo-item.is-color-white { background-color: #FEFEFE; }

スタイルを適用すると次のような表示になります。

メモアプリっぽくなりましたね!

次のステップに進みましょう。

次へ