ReactでTrelloみたいなToDoリスト＜3＞ prop-types


## 関連記事

* [Reactのスタート](http://qiita.com/park-jh/items/63bf0f013c998c5572aa)
* [Reactのプロジェクト](http://qiita.com/park-jh/items/488d43e2d0b87991207e)
* [Reactで挨拶](http://qiita.com/park-jh/items/bffaec5d05a96850e91b)
* [ReactでTrelloみたいなToDoリスト＜1＞ props](http://qiita.com/park-jh/items/f3f6162cee982eb9e6f3)
* [ReactでTrelloみたいなToDoリスト＜2＞ state](http://qiita.com/park-jh/items/0b71fd9c7e6cc8cbe844)
* ReactでTrelloみたいなToDoリスト＜3＞ prop-types
* [ReactでTrelloみたいなToDoリスト＜4＞ immutability-helper](http://qiita.com/park-jh/items/6166434f80bc186f8c77)

## PropTypes Validatorを使う。

今まで修正していないprops関連eslintエラーを直してみよう。
親コンポーネントから子コンポーネント順で修正する。

まず、KanbanBoard.jsのeslintエラーだ。

```vim
src/components/KanbanBoard.js|8 col 15 error| Property should be placed on a new line (react/jsx-first-prop-new-line)
src/components/KanbanBoard.js|8 col 25 error| Prop `title` must be placed on a new line (react/jsx-max-props-per-line)
src/components/KanbanBoard.js|9 col 24 error| 'cards' is missing in props validation (react/prop-types)
src/components/KanbanBoard.js|9 col 30 error| 'cards.filter' is missing in props validation (react/prop-types)
src/components/KanbanBoard.js|9 col 37 error| Unexpected parentheses around single function argument having a body with no curly braces (arrow-parens)
src/components/KanbanBoard.js|12 col 15 error| Property should be placed on a new line (react/jsx-first-prop-new-line)
src/components/KanbanBoard.js|12 col 32 error| Prop `title` must be placed on a new line (react/jsx-max-props-per-line)
src/components/KanbanBoard.js|13 col 24 error| 'cards' is missing in props validation (react/prop-types)
src/components/KanbanBoard.js|13 col 30 error| 'cards.filter' is missing in props validation (react/prop-types)
src/components/KanbanBoard.js|13 col 37 error| Unexpected parentheses around single function argument having a body with no curly braces (arrow-parens)
src/components/KanbanBoard.js|16 col 15 error| Property should be placed on a new line (react/jsx-first-prop-new-line)
src/components/KanbanBoard.js|16 col 25 error| Prop `title` must be placed on a new line (react/jsx-max-props-per-line)
src/components/KanbanBoard.js|17 col 24 error| 'cards' is missing in props validation (react/prop-types)
src/components/KanbanBoard.js|17 col 30 error| 'cards.filter' is missing in props validation (react/prop-types)
src/components/KanbanBoard.js|17 col 37 error| Unexpected parentheses around single function argument having a body with no curly braces (arrow-parens)
```

全propertyのエラーなので多く見えるが、種類は以下の4つだ。

* [react/jsx-first-prop-new-line](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-first-prop-new-line.md)
* [react/jsx-max-props-per-line](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-max-props-per-line.md)
* [react/prop-types](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-max-props-per-line.md)
* [arrow-parens](http://eslint.org/docs/rules/arrow-parens)

各エラーの意味を訳すとこうなる。

* propertyは新しいラインの最初に書けなければならない。
* 1ラインで書けるproperty個数の最大値を超えている。（defaultが1だった。）
* props validationが抜けている。
* arrow関数の引数に括弧が必要だ。

大体タイトルだけ訳してもその意味を分かるが、arrow関数だけはややこしい。
正解を先に確認しよう。

```diff:src/components/KanbanBoard.js
diff --git a/src/components/KanbanBoard.js b/src/components/KanbanBoard.js
index da98d72..b89409d 100644
--- a/src/components/KanbanBoard.js
+++ b/src/components/KanbanBoard.js
@@ -1,20 +1,30 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';
 import List from './List';

 class KanbanBoard extends Component {
   render() {
     return (
       <div className="app">
-        <List id="todo" title="To Do" cards={
-            this.props.cards.filter((card) => card.status === 'todo')
+        <List
+          id="todo"
+          title="To Do"
+          cards={
+            this.props.cards.filter(card => card.status === 'todo')
           }
         />
-        <List id="in-progress" title="In Progress" cards={
-            this.props.cards.filter((card) => card.status === 'in-progress')
+        <List
+          id="in-progress"
+          title="In Progress"
+          cards={
+            this.props.cards.filter(card => card.status === 'in-progress')
           }
         />
-        <List id="done" title="Done" cards={
-            this.props.cards.filter((card) => card.status === 'done')
+        <List
+          id="done"
+          title="Done"
+          cards={
+            this.props.cards.filter(card => card.status === 'done')
           }
         />
       </div>
@@ -22,4 +32,8 @@ class KanbanBoard extends Component {
   }
 }

+KanbanBoard.propTypes = {
+  cards: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
 export default KanbanBoard;
```

arrow-parensの場合だが、タイトルだけでは誤解しやすい。
ちょっとだけ内容を見ると、

```
Arrow functions can omit parentheses when they have exactly one parameter.
In all other cases the parameter(s) must be wrapped in parentheses.
This rule enforces the consistent use of parentheses in arrow functions.
```

Arrow関数は1つのパラメータだけを持っている場合は括弧の省略ができる。
この場合を除いて括弧でwrappedしなければならない。
このルールはarrow関数内括弧の正確な使いを強制する。

~~要するに、パラメータが1つの場合は括弧を使うな！そして２つ以上の場合は必ず括弧を使え!と言う意味だ。~~
（2017.7.6 訂正）
このルールは２つのstringオプションを持っている。
"always"と"as-needed"というオプションだ。
"always"がデフォルトだが、これは必ず括弧を使わないとエラーになる。
もしデフォルトであれば、パラメータが1つの場合も括弧を使わなければならない。
"as-needed"はパラメータが1つの場合は括弧を使うことを禁止する。
airbnbのスタイルガイドを使っているので、デフォルトのeslint設定が上書きされていることを忘れちゃダメだ。
airbnbのスタイルガイドでは"as-needed"オプションを使っていてこれに合わない場合はエラーにしている。

```vim
  ▸ escope/                    | 18     'arrow-body-style': ['error', 'as-needed', {
  ▾ eslint-config-airbnb-base/ | 19       requireReturnForObjectLiteral: false,
    ▸ node_modules/.bin/       | 20     }],
    ▾ rules/                   | 21
        best-practices.js      | 22     // require parens in arrow function arguments
        errors.js              | 23     // http://eslint.org/docs/rules/arrow-parens
        es6.js                 | 24     'arrow-parens': ['error', 'as-needed', {
        imports.js             | 25       requireForBlockBody: true,
        node.js                | 26     }],
```

airbnbのスタイルガイドを使っている場合は、括弧を付けて置いてもarrow-parensのeslintエラーが出る。
”パラメータが1つの場合は括弧を使うな！”とね。
他のコンポーネントのeslintエラーも同じものなので、説明は省略する。

```diff:src/components/List.js
diff --git a/src/components/List.js b/src/components/List.js
index 38b60f9..c548d80 100644
--- a/src/components/List.js
+++ b/src/components/List.js
@@ -1,19 +1,19 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';
 import Card from './Card';

 class List extends Component {
   render() {
-    const cards = this.props.cards.map((card) => {
-      return (<Card
+    const cards = this.props.cards.map(card =>
+      (<Card
         key={card.id}
         id={card.id}
         title={card.title}
         description={card.description}
         color={card.color}
         tasks={card.tasks}
-      />
-      );
-    });
+      />),
+    );

     return (
       <div className="list">
@@ -24,4 +24,9 @@ class List extends Component {
   }
 }

+List.propTypes = {
+  title: PropTypes.string.isRequired,
+  cards: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
 export default List;
```

```diff:src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index c3115d7..38c2cbc 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -1,4 +1,5 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';
 import Marked from 'marked';
 import CheckList from './CheckList';

@@ -33,7 +34,7 @@ class Card extends Component {
       bottom: 0,
       left: 0,
       width: 7,
-      backgroundColor: this.props.color
+      backgroundColor: this.props.color,
     };

     return (
@@ -52,4 +53,12 @@ class Card extends Component {
   }
 }

+Card.propTypes = {
+  id: PropTypes.number.isRequired,
+  title: PropTypes.string.isRequired,
+  description: PropTypes.string.isRequired,
+  color: PropTypes.string.isRequired,
+  tasks: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
 export default Card;
```

```diff:src/components/CheckList.js
diff --git a/src/components/CheckList.js b/src/components/CheckList.js
index ca1bf58..4de883f 100644
--- a/src/components/CheckList.js
+++ b/src/components/CheckList.js
@@ -1,8 +1,9 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';

 class CheckList extends Component {
   render() {
-    const tasks = this.props.tasks.map((task) => (
+    const tasks = this.props.tasks.map(task => (
       <li key={task.id} className="checklist__task">
         <input type="checkbox" defaultChecked={task.done} />
         {task.name}{' '}
@@ -23,4 +24,8 @@ class CheckList extends Component {
   }
 }

+CheckList.propTypes = {
+  tasks: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
 export default CheckList;
```

## カスタムPropTypes Validatorを定義する。

[custom validator](http://qiita.com/park-jh/items/f06b9ce4a7a698b5c6c9)については別途に記事を作成したので、そちらを参考にしよう。
おまけにconstructorの引数もpropsに修正した。

```diff:src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 38c2cbc..5025f5f 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -4,8 +4,8 @@ import Marked from 'marked';
 import CheckList from './CheckList';

 class Card extends Component {
-  constructor(...args) {
-    super(...args);
+  constructor(props) {
+    super(props);
     this.state = {
       showDetails: false,
     };
@@ -53,9 +53,31 @@ class Card extends Component {
   }
 }

+function createCustomPropType(isRequired) {
+  return (props, propName, componentName) => {
+    const prop = props[propName];
+    if (prop == null) {
+      if (isRequired) {
+        return new Error(
+          `The prop \`${propName}\` is marked as required in \`${componentName}\`, but its value is \`undefined\`.`,
+        );
+      }
+    } else if (typeof prop !== 'string' || prop.length > 80) {
+      return new Error(
+        `${propName} in ${componentName} is longer than 80 characters`,
+      );
+    }
+    return null;
+  };
+}
+
+// Using the factory, create two different versions of your prop type
+const customPropType = createCustomPropType(false);
+customPropType.isRequired = createCustomPropType(true);
+
 Card.propTypes = {
   id: PropTypes.number.isRequired,
-  title: PropTypes.string.isRequired,
+  title: customPropType.isRequired,
   description: PropTypes.string.isRequired,
   color: PropTypes.string.isRequired,
   tasks: PropTypes.arrayOf(PropTypes.object).isRequired,
```