Make it to make it

いろいろ作ってアウトプットするブログ

React hooks入門からToDoアプリまで

今回、React hooksによるToDoアプリを作って、従来のclassコンポーネントによる書き方と比べて、そのように書き方が変わったかというところを検証してみた。

結果からいうと、React hooksを導入して書いた方が圧倒的に書きやすかった。 コードがはるかにリーダブルな他に、書いたコードの行数も30行少ないものだった。

github.com

なぜReact hooksが導入されたか

  • thisがわずらわしかった
  • super(props)もわずらわしかった
    • stateをclass fieldとして書くことで解決はしていたものの
  • Lifecycleメソッド内で同じロジックを書くのがわずらわしかった
  • ロジックとUIを分離するのが難しかった
    • Higher Order Comopnents (HOC)
    • Render Props

ToDoアプリ実装例

Class components

まず先ず、stateフィールドをconstructor外でclass fieldとして書く際に、babelプラグインが必要となる。

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

従来の書き方だとthisによる結びつきが多く、コードが読みづらい。 stateの展開時やコンポーネント内のメソッドを参照する際にいちいちthisを書かないといけない。 この不便さはReact hooks使用時に気づいた。

TodoLegacy.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';

function TodoItems({ items, checkFunc, isDoneItems }) {
  const generateId = () =>
    Math.random()
      .toString(36)
      .substr(2, 9);

  return (
    <ul className="pl-0 list-none">
      {items.map((item, index) => (
        <li key={`${item}-${generateId}`}>
          <label htmlFor={`${item}-${index}`}>
            <input
              type="checkbox"
              id={`${item}-${index}`}
              onChange={e => checkFunc(e, item, isDoneItems)}
            />
            <span className={isDoneItems && 'line-through'}>{item}</span>
          </label>
        </li>
      ))}
    </ul>
  );
}

TodoItems.propTypes = {
  items: PropTypes.array,
  checkFunc: PropTypes.func,
  isDoneItems: PropTypes.bool,
};

class TodoLegacy extends Component {
  state = {
    inputValue: '',
    todoItems: ['apple', 'banana', 'clementine'],
    doneTodoItems: [],
  };

  changeInputValue = e => {
    this.setState({
      inputValue: e.target.value,
    });
  };

  addItem = e => {
    if (e.keyCode !== 13) return;
    const { todoItems } = this.state;
    this.setState({
      inputValue: '',
      todoItems: [...todoItems, e.target.value],
    });
  };

  checkItem = (e, val, isDoneItems) => {
    if (!e.target.checked) return;
    const { todoItems, doneTodoItems } = this.state;
    const leftTodoItems = [...todoItems].filter(todoItem => todoItem !== val);
    const checkedTodoItems = [...todoItems].find(todoItem => todoItem === val);
    const leftDoneTodoItems = [...doneTodoItems].filter(
      doneTodoItem => doneTodoItem !== val
    );
    const checkedDoneTodoItems = [...doneTodoItems].find(
      doneTodoItem => doneTodoItem === val
    );
    setTimeout(() => {
      if (!isDoneItems) {
        this.setState({
          todoItems: [...leftTodoItems],
          doneTodoItems: [...doneTodoItems, checkedTodoItems],
        });
      } else {
        this.setState({
          todoItems: [...todoItems, checkedDoneTodoItems],
          doneTodoItems: [...leftDoneTodoItems],
        });
      }
    }, 400);
  };

  render() {
    const { inputValue, todoItems, doneTodoItems } = this.state;

    return (
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={this.changeInputValue}
          onKeyDown={e => this.addItem(e)}
        />
        <section>
          <h3>ToDo</h3>
          <TodoItems items={todoItems} checkFunc={this.checkItem} />
        </section>
        {doneTodoItems[0] && (
          <section>
            <h3>Done</h3>
            <TodoItems
              items={doneTodoItems}
              checkFunc={this.checkItem}
              isDoneItems
            />
          </section>
        )}
      </div>
    );
  }
}

export default TodoLegacy;

Functional components with react hooks

React hooksを用いた書き方ではthisを使う必要が一切なかった。thisの縛りから解放されたのに驚いた。 また状態の更新時に従来のように、複数行にかけてthis.setState({ key: 'value' })と書く必要がなく、一つひとつのstateに個別で作ったカスタムのsetterで更新ができるのもよい。

Todo.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';

function TodoItems({ items, checkFunc, isDoneItems }) {
  const generateId = () =>
    Math.random()
      .toString(36)
      .substr(2, 9);

  return (
    <ul className="pl-0 list-none">
      {items.map((item, index) => (
        <li key={`${item}-${generateId}`}>
          <label htmlFor={`${item}-${index}`}>
            <input
              type="checkbox"
              id={`${item}-${index}`}
              onChange={e => checkFunc(e, item, isDoneItems)}
            />
            <span className={isDoneItems && 'line-through'}>{item}</span>
          </label>
        </li>
      ))}
    </ul>
  );
}

TodoItems.propTypes = {
  items: PropTypes.array,
  checkFunc: PropTypes.func,
  isDoneItems: PropTypes.bool,
};

function Todo() {
  const [inputValue, setInputValue] = useState('');
  const [todoItems, setTodoItems] = useState(['apple', 'banana', 'clementine']);
  const [doneTodoItems, setDoneTodoItems] = useState([]);

  const changeInputValue = e => {
    setInputValue(e.target.value);
  };

  const addItem = e => {
    if (e.keyCode !== 13) return;
    setInputValue('');
    setTodoItems([...todoItems, e.target.value]);
  };

  const checkItem = (e, val, isDoneItems) => {
    if (!e.target.checked) return;
    const leftTodoItems = [...todoItems].filter(todoItem => todoItem !== val);
    const checkedTodoItems = [...todoItems].find(todoItem => todoItem === val);
    const leftDoneTodoItems = [...doneTodoItems].filter(
      doneTodoItem => doneTodoItem !== val
    );
    const checkedDoneTodoItems = [...doneTodoItems].find(
      doneTodoItem => doneTodoItem === val
    );
    setTimeout(() => {
      if (!isDoneItems) {
        setTodoItems([...leftTodoItems]);
        setDoneTodoItems([...doneTodoItems, checkedTodoItems]);
      } else {
        setTodoItems([...todoItems, checkedDoneTodoItems]);
        setDoneTodoItems([...leftDoneTodoItems]);
      }
    }, 400);
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={changeInputValue}
        onKeyDown={e => addItem(e)}
      />
      <section>
        <h3>ToDo</h3>
        <TodoItems items={todoItems} checkFunc={checkItem} />
      </section>
      {doneTodoItems[0] && (
        <section>
          <h3>Done</h3>
          <TodoItems items={doneTodoItems} checkFunc={checkItem} isDoneItems />
        </section>
      )}
    </div>
  );
}

export default Todo;