LoginSignup
4
7

More than 5 years have passed since last update.

ReactでTrelloみたいなToDoリスト<2> state

Last updated at Posted at 2017-06-24

関連記事

stateについて

コンポーネント間データの受け渡しに使ったpropsは変更不可(immutable)だった。
変更可能(mutable)なstateについて学んでみよう。

Reactのコンポーネントはthis.stateを用いて、色んなデータを持ちられる。
this.stateは、this.setState()を呼び出して変えられる。
stateが変えたらトリガーが発動されて、該当コンポーネントとその子コンポーネントが再びレンダリングされる。
Reactは仮想DOMを使っているので、この作業が早い。

toggleされるカード

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 61629cf..3905582 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -1,26 +1,38 @@
 import React, { Component } from 'react';
 import CheckList from './CheckList';

 class Card extends Component {
+  constructor(...args) {
+    super(...args);
+    this.state = {
+      showDetails: false,
+    };
+  }
+
   render() {
-    return (
-      <div className="card">
-        <div className="card__title">{this.props.title}</div>
+    let cardDetails;
+    if (this.state.showDetails) {
+      cardDetails = (
         <div className="card__details">
           {this.props.description}
           <CheckList cardId={this.props.id} tasks={this.props.tasks} />
         </div>
+      );
+    }
+
+    return (
+      <div className="card">
+        <div
+          className="card__title"
+          onClick={() => this.setState({ showDetails: !this.state.showDetails })}
+          role="presentation"
+        >
+          {this.props.title}
+        </div>
+        {cardDetails}
       </div>
     );
   }
 }

本のサンプルではroleを指定していないが、この状態ではeslintエラーが出る。

src/components/Card.js|25 col 9 error| Static HTML elements with event handlers require a role. (jsx-a11y/no-static-element-interactions)

button、link、checkboxのような行動要素についてはroleを明記しないといけない。
詳細は下記のリンクで確認しよう。

:bulb: HTMLのroleについて


:point_right: role
HTML要素に役割を与えるための属性である。
role="presentation"というのはこのHTML要素が装飾目的で使われていると言う意味だ。
例えば、imgタグの場合はalt="代替テキスト"でこのタグが何なのか説明しているが、こんなときにrole="presentation"を使える。
詳細は次のリンクで確認しよう。
* http://blog.e-riverstyle.com/2011/03/rolepresentationwaiaria.html
* http://www.webcreativepark.net/html/wai-aria/role/

イベントハンドラの作成

onClickというイベントについてインライン関数で作成したが、これを別途の関数で取り出してみよう。

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 3905582..e13542e 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -9,6 +9,10 @@ class Card extends Component {
     };
   }

+  toggleDetails() {
+    this.setState({ showDetails: !this.state.showDetails });
+  }
+
   render() {
     let cardDetails;
     if (this.state.showDetails) {
@@ -24,7 +28,7 @@ class Card extends Component {
       <div className="card">
         <div
           className="card__title"
-          onClick={() => this.setState({ showDetails: !this.state.showDetails })}
+          onClick={this.toggleDetails.bind(this)}
           role="presentation"
         >
           {this.props.title}

:bulb: this.toggleDetailsではなくthis.toggleDetails.bind(this)の理由


:point_right: this
thisはオブジェクト自分自身を指しているからthis.toggleDetailsでいいではないかと思うかもしれない。
それは嘘ではない。javascriptでも同じだ。しかし、javascriptはイベントの発生元もthisで指す場合がある。
onClickというイベントをハンドルするので、ここでのthisはonClickイベントを指すことになる。
onClickのイベントにはsetStateという関数がないので、「TypeError: Cannot read property 'setState' of null」というエラーが発生する。
bind関数を利用することで、関数toggleDetailsの中のthisがbind関数で指定した引数(ここではthis)に紐付けされる。

またeslintエラーが出る。
以前と同じくpropsに関するエラーは無視する。
しかし、エラーの中ではこんなエラーもあった。

src/components/Card.js|31 col 11 error| JSX props should not use .bind() (react/jsx-no-bind)

jsxのpropsではbindを使うな!とね。
これを直してみよう。

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 3905582..5f891ac 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -7,6 +7,11 @@ class Card extends Component {
     this.state = {
       showDetails: false,
     };
+    this.toggleDetails = this.toggleDetails.bind(this);
+  }
+
+  toggleDetails() {
+    this.setState({ showDetails: !this.state.showDetails });
   }

   render() {
@@ -24,7 +29,7 @@ class Card extends Component {
       <div className="card">
         <div
           className="card__title"
-          onClick={() => this.setState({ showDetails: !this.state.showDetails })}
+          onClick={this.toggleDetails}
           role="presentation"
         >
           {this.props.title}

指摘された通りにjsxのpropsでbindを使わずにconstructorで使ったら、エラーが消えた。

:bulb: なぜeslintではjsxのpropsにbindを使うとエラーになるのか?


constructorに書くのがもっと冗長に見える。
しかし、同じ関数をいろんなイベントで使うことになるなら話は別だ。
jsxのpropsにいちいち書くよりはconstructorの1ヶ所に書いた方が重複を避けられる。

カードが開いているかを確認

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 5f891ac..8b3671e 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -28,7 +28,7 @@ class Card extends Component {
     return (
       <div className="card">
         <div
-          className="card__title"
+          className={this.state.showDetails ? 'card__title card__title--is-open' : 'card__title'}
           onClick={this.toggleDetails}
           role="presentation"
         >

動的HTMLレンダリング

ReactはXSS攻撃を防ぐためにjsxでHTMLタグをレンダリングすることを禁止している。
しかし、HTMLレンダリングが必要な場合もある。
Markdown表記もそのような場合だ。
テストのためにcardsListのデータにMarkdown表記で入れて確認してみよう。

src/index.js
diff --git a/src/index.js b/src/index.js
index eeb27fe..c7f23ee 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,14 +8,14 @@ const cardsList = [
   {
     id: 1,
     title: 'Read the Book',
-    description: 'I should read the whole book',
+    description: 'I should read the **whole** book',
     status: 'in-progress',
     tasks: [],
   },
   {
     id: 1,
     title: 'Write some code',
-    description: 'Code along with the samples in the book',
+    description: 'Code along with the samples in the book. The complete source can be found at [github](https://github.com/pro-react)',
     status: 'todo',
     tasks: [
       {

Markdownを使うために関連パッケージを追加して、Cardコンポーネントから使うように修正する。

% yarn add marked

本のサンプルでは「import marked from 'marked'」で出ているがmarkedというmoduleがないよ!と怒られる。
「import Marked from 'marked'」の誤字なので修正しよう。

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 8b3671e..48ad868 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -1,4 +1,5 @@
 import React, { Component } from 'react';
+import Marked from 'marked';
 import CheckList from './CheckList';

 class Card extends Component {
@@ -19,7 +20,7 @@ class Card extends Component {
     if (this.state.showDetails) {
       cardDetails = (
         <div className="card__details">
-          {this.props.description}
+          {Marked(this.props.description)}
           <CheckList cardId={this.props.id} tasks={this.props.tasks} />
         </div>
       );

ここまでにしてブラウザで確認すると、こうなる。
스크린샷 2017-06-23 18.09.16.png

Reactはjsxの中でHTMLタグをレンダリングすることを禁止しているので、タグがテキストとして扱われる。
しかし、dangerouslySetInnerHTMLを使えば、HTMLタグのレンダリングが可能になる。

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 8b3671e..4c2d3c1 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -1,4 +1,5 @@
 import React, { Component } from 'react';
+import Marked from 'marked';
 import CheckList from './CheckList';

 class Card extends Component {
@@ -19,7 +20,7 @@ class Card extends Component {
     if (this.state.showDetails) {
       cardDetails = (
         <div className="card__details">
-          {this.props.description}
+          <span dangerouslySetInnerHTML={{ __html: Marked(this.props.description) }} />
           <CheckList cardId={this.props.id} tasks={this.props.tasks} />
         </div>
       );

正常に表示されることを確認しよう。
스크린샷 2017-06-23 18.35.29.png

インラインスタイルを使ってカードの色を指定

データに色を追加する。

src/index.js
diff --git a/src/index.js b/src/index.js
index eeb27fe..dd395b8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,14 +8,16 @@ const cardsList = [
   {
     id: 1,
     title: 'Read the Book',
     description: 'I should read the **whole** book',
+    color: '#BD8D31',
     status: 'in-progress',
     tasks: [],
   },
   {
     id: 1,
     title: 'Write some code',
     description: 'Code along with the samples in the book',
+    color: '#3A7E28',
     status: 'todo',
     tasks: [
       {

propsでCardコンポーネントに色も伝えるように修正する。

src/components/List.js
diff --git a/src/components/List.js b/src/components/List.js
index 238333e..a3bf1cd 100644
--- a/src/components/List.js
+++ b/src/components/List.js
@@ -8,6 +8,7 @@ class List extends Component {
         id={card.id}
         title={card.title}
         description={card.description}
+        color={card.color}
         tasks={card.tasks}
       />
       );

インラインスタイルを持つオブジェクトを生成して、このオブジェクトをインラインで使うdivタグを生成する。

src/components/Card.js
diff --git a/src/components/Card.js b/src/components/Card.js
index 4c2d3c1..5e30838 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -26,8 +26,19 @@ class Card extends Component {
       );
     }

+    const sideColor = {
+      position: 'absolute',
+      zIndex: -1,
+      top: 0,
+      bottom: 0,
+      left: 0,
+      width: 7,
+      backgroundColor: this.props.color
+    };
+
     return (
       <div className="card">
+        <div style={sideColor} />
         <div
           className={this.state.showDetails ? 'card__title card__title--is-open' : 'card__title'}
           onClick={this.toggleDetails}

結果
스크린샷 2017-06-23 18.46.41.png

カードに入力フィールドを追加

src/components/CheckList.js
diff --git a/src/components/CheckList.js b/src/components/CheckList.js
index 5ffe6ba..3b878a7 100644
--- a/src/components/CheckList.js
+++ b/src/components/CheckList.js
@@ -14,13 +14,14 @@ class CheckList extends Component {
     return (
       <div className="checklist">
         <ul>{tasks}</ul>
+        <input
+          type="text"
+          className="checklist--add-task"
+          placeholder="Type then hit Enter to add a task"
+        />
       </div>
     );
   }
 }

 export default CheckList;

結果
스크린샷 2017-06-23 22.44.02.png

Keys

ブラウザのdeveloper toolsで確認すると、Consoleで以下のようなWarningが表示される。

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `List`. See https://fb.me/react-warning-keys for more information.

arrayやiterator中の子ノードはuniqueなkey属性を持たなければならない。
識別子(uniqueなkey)があれば、特定のノードの探索が早くなる。
Reactにてレンダリングのコストは低くない。
そんなコストを少しでも減らすためには識別子が必要なのだ。
識別子を入れてみよう。

src/components/CheckList.js
diff --git a/src/components/CheckList.js b/src/components/CheckList.js
index 3b878a7..991d734 100644
--- a/src/components/CheckList.js
+++ b/src/components/CheckList.js
@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
 class CheckList extends Component {
   render() {
     const tasks = this.props.tasks.map((task) => (
-      <li className="checklist__task">
+      <li key={task.id} className="checklist__task">
         <input type="checkbox" defaultChecked={task.done} />
         {task.name}{' '}
         <span className="checklist__task--remove" />
       </li>
     ));
src/components/List.js
diff --git a/src/components/List.js b/src/components/List.js
index a3bf1cd..38b60f9 100644
--- a/src/components/List.js
+++ b/src/components/List.js
@@ -5,6 +5,7 @@ class List extends Component {
   render() {
     const cards = this.props.cards.map((card) => {
       return (<Card
+        key={card.id}
         id={card.id}
         title={card.title}
         description={card.description}

developer toolsにてWarningが消えた。

:bulb: よく使うarrayのコールバック関数

:point_right: forEach

array.forEach(callback [,that])
    array: 配列オブジェクト
    callback: 個々の要素を処理するための関数
    that: 関数callbackの中でthisが示すオブジェクト
    戻り値: 個々の要素

:point_right: map

array.map(callback [,that])
    array: 配列オブジェクト
    callback: 個々の要素を加工するための関数
    that: 関数callbackの中でthisが示すオブジェクト
    戻り値: 配列

:point_right: some

array.some(callback [,that])
    array: 配列オブジェクト
    callback: 個々の要素を判定するための関数
    that: 関数callbackの中でthisが示すオブジェクト
    戻り値: つでも条件に合致した場合=関数callbackが一度でもtrueを返した場合はtrueそうではない場合はfalse

:point_right: filter

array.filter(callback [,that])
    array: 配列オブジェクト
    callback: 個々の要素を判定するための関数
    that: 関数callbackの中でthisが示すオブジェクト
    戻り値: 条件に合致した要素だけを取り出した配列
4
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7