React hooks入門からToDoアプリまで
今回、React hooksによるToDoアプリを作って、従来のclassコンポーネントによる書き方と比べて、そのように書き方が変わったかというところを検証してみた。
結果からいうと、React hooksを導入して書いた方が圧倒的に書きやすかった。 コードがはるかにリーダブルな他に、書いたコードの行数も30行少ないものだった。
なぜ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;