【React】シンプルな「TODOアプリ」を作ってみる

【React】シンプルな「TODOアプリ」を作ってみる

こんにちわ。kyamashitaです。

「React」の連載第三弾ということで、シンプルなTODOアプリを実際に作ってみたいと思います。

完成イメージ

ざっとこんな感じのイメージで作ってみます。

kyamashita_2022206_01.png コンポーネントは「入力用」「フィルター用」「一覧表示用」の3つです。

構成

前回の記事を参考に雛形を作り、以下のファイルを修正、追加します。

  src/
    App.css (修正)
    App.js (修正)
    InputComponent.js (追加)
    InputComponent.css (追加)
    FilterComponent.js (追加)
    FilterComponent.css (追加)
    ListComponent.js (追加)
    ListComponent.css (追加)

各コンポーネントの表示

まずは各コンポーネントをApp.jsで読込表示させてみます。

各コンポーネントをとりあえず以下のように作成します。

▼InputComponent.js
import styles from "./InputComponent.css"

export default function InputComponent() {
    return (
        <h1>InputComponent</h1>
    )
}

▼FilterComponent.js
import styles from "./FilterComponent.css"

export default function FilterComponent() {
    return (
        <h1>FilterComponent</h1>
    )
}

▼ListComponent.js
import styles from "./ListComponent.css"

export default function ListComponent() {
    return (
        <h1>ListComponent</h1>
    )
}

App.cssをクリアし、App.jsを以下のように編集し各コンポーネントを読み込みます。

▼App.css
クリアする

▼App.js
import './App.css'

import InputComponent from "./InputComponent"
import FilterComponent from "./FilterComponent"
import ListComponent from "./ListComponent"

export default function App() {
  return (
      <div>
        <InputComponent/>
        <FilterComponent/>
        <ListComponent/>
      </div>
  )
}

このように表示されると思います。

kyamashita_2022206_02.png

要素のコーディング

各コンポーネントに要素をコーディングします。

▼InputComponent.js
export default function InputComponent() {
    return (
        <form>
            <input type="text" placeholder="タイトル"/>
            <button>保存</button>
        </form>
    )
}

▼FilterComponent.js
export default function FilterComponent() {
    return (
        <div>
            <button>すべて</button>
            <button>完了</button>
            <button>未完了</button>
        </div>
    )
}

▼ListComponent.js
export default function ListComponent() {
    return (
        <div>
            <ul>
                <li>
                    <span>タイトル</span>
                    <span><input type="checkbox"/></span>
                    <span><button>削除</button></span>
                </li>
                <li>
                    <span>タイトル</span>
                    <span><input type="checkbox"/></span>
                    <span><button>削除</button></span>
                </li>
                <li>
                    <span>タイトル</span>
                    <span><input type="checkbox"/></span>
                    <span><button>削除</button></span>
                </li>
            </ul>
        </div>
    )
}

ひとまずこのようになると思います。

kyamashita_2022206_03.png

データを保持して一覧で表示する

ステートフックと副作用フックを使用します。

フックについてはこちらを参照

App.jsでフックをインポートし、データを初期化します。

import React, {useEffect, useState} from 'react'

const [todoItems, setTodoItems] = useState([])

useEffect(() => {
    (async () => {
        setTodoItems([
            {id:1,title:'あああ',is_done:false},
            {id:2,title:'いいい',is_done:true},
            {id:3,title:'ううう',is_done:false},
            {id:4,title:'えええ',is_done:true},
            {id:5,title:'おおお',is_done:false},
        ])
    })()
}, [])

設定したデータを一覧表示用のコンポーネントに渡します。

コンポーネントはprops(プロパティ)を引数にすることで、データの受け渡しができます。

▼App.js
<ListComponent todoItems={todoItems}/>

▼ListComponent.js
export default function ListComponent(props)
※props.todoItems とすると使用できます。ただし読み取り専用です。

では受け取ったデータを一覧表示にします。

mapを使用して繰り返し表示しています。

export default function ListComponent(props) {
    return (
        <div>
            <ul>
                {props.todoItems.map(todoItem =>
                    <li key={todoItem.id}>
                        <span>{todoItem.title}</span>
                        <span><input type="checkbox" checked={todoItem.is_done}/></span>
                        <span><button>削除</button></span>
                    </li>
                )}
            </ul>
        </div>
    )
}
kyamashita_2022206_04.png

入力した内容を保存できるようにする

まずは保存ボタンを押した時にタイトルを取得してみます。

▼InputComponent.js
const [title, setTitle] = useState('')

// テキストのonChange用の処理
const changeTitle = (e) => {
    setTitle(e.target.value)
}

// 保存ボタンのonClick用のメソッド
const save = (e) => {
    e.preventDefault()
    console.log(title)
}

<input type="text" placeholder="タイトル" value={title} onChange={changeTitle}/>
<button onClick={save}>保存</button>

これで保存を押した時に入力したタイトルがコンソールに表示されていると思います。

タイトル取得は確認したので、保存してみます。

▼App.js
リスト初期化部分は削除します。
useEffect(() => {
    (async () => {
        setTodoItems([
            {id:1,title:'あああ',is_done:false},
            {id:2,title:'いいい',is_done:true},
            {id:3,title:'ううう',is_done:false},
            {id:4,title:'えええ',is_done:true},
            {id:5,title:'おおお',is_done:false},
        ])
    })()
}, [])

更新用のメソッドを作っておきコンポーネントに渡します。
const addTodoItem = (title) => {
    setTodoItems([...todoItems,{id:todoItems.length+1, title:title, is_done:false}])
}

<InputComponent addTodoItem={addTodoItem}/>

▼InputComponent.js
渡されたメソッドを保存処理で使用します。保存後は入力を空にします。
const save = (e) => {
    e.preventDefault()
    props.addTodoItem(title)
    setTitle('')
}

これで保存ができるようになったと思います。

※ブラウザ更新するとクリアされます

一覧でステータスを変更、データを削除する

▼App.js
ステータス更新と削除用のメソッドを作ります。

const updateStatusTodoItem = (id) => {
    setTodoItems(todoItems.map(todoItem => {
        if (todoItem.id === id) {
            todoItem.is_done = !todoItem.is_done
        }
        return todoItem
    }))
}

const removeTodoItem = (id) => {
    setTodoItems(todoItems.filter(todoItem => todoItem.id !== id))
}

ListComponentに渡します。

<ListComponent todoItems={todoItems} updateStatusTodoItem={updateStatusTodoItem} removeTodoItem={removeTodoItem}/>


▼ListComponent.js

チェックのonChangeと削除ボタンのonClickにそれぞれ設定します。

<span>
    <input type="checkbox"
           checked={todoItem.is_done}
           onChange={e => {
               props.updateStatusTodoItem(todoItem.id)
           }}
    />
</span>
<span>
    <button
        onClick={e => {
            e.preventDefault()
            props.removeTodoItem(todoItem.id)
        }}
    >
        削除
    </button>
</span>

これでステータス変更と削除ができるようになと思います。

フィルタ

▼App.js
フィルタ用のステートを追加します。

// 0:すべて 1:完了 2:未完了
const [filterStatus, setFilterStatus] = useState(0)

コンポーネントに渡します。
<FilterComponent setFilterStatus={setFilterStatus}/>
<ListComponent todoItems={todoItems} updateStatusTodoItem={updateStatusTodoItem} removeTodoItem={removeTodoItem} filterStatus={filterStatus}/>

▼FilterComponent.js

各ボタンを押した時の処理を追加します。
export default function FilterComponent(props) {
    const all = () => {
        props.setFilterStatus(0)
    }
    const done = () => {
        props.setFilterStatus(1)
    }
    const unDone = () => {
        props.setFilterStatus(2)
    }
    return (
        <div>
            <button onClick={all}>すべて</button>
            <button onClick={done}>完了</button>
            <button onClick={unDone}>未完了</button>
        </div>
    )
}


▼ListComponent.js
フィルターのステートに合わせて表示するようにmap内を修正します。

export default function ListComponent(props) {
    return (
        <div>
            <ul>
                {props.todoItems.map(todoItem => {
                    if (props.filterStatus === 1 && !todoItem.is_done) {
                        return false
                    }
                    if (props.filterStatus === 2 && todoItem.is_done) {
                        return false
                    }
                    return (
                        <li key={todoItem.id}>
                            <span>{todoItem.title}</span>
                            <span>
                                <input type="checkbox"
                                       checked={todoItem.is_done}
                                       onChange={e => {
                                           props.updateStatusTodoItem(todoItem.id)
                                       }}
                                />
                            </span>
                            <span>
                                <button
                                    onClick={e => {
                                        e.preventDefault()
                                        props.removeTodoItem(todoItem.id)
                                    }}
                                >
                                    削除
                                </button>
                            </span>
                        </li>
                    )
                })}
            </ul>
        </div>
    )
}

これでフィルタが動作するようになったと思います。

修正、追加ファイル

少しだけスタイル調整した最終的なコードはこちらになります。

▼App.js
import './App.css'
import React, {useEffect, useState} from 'react'
import InputComponent from "./InputComponent"
import FilterComponent from "./FilterComponent"
import ListComponent from "./ListComponent"
export default function App() {
    const [todoItems, setTodoItems] = useState([])
    // 0:すべて 1:完了 2:未完了
    const [filterStatus, setFilterStatus] = useState(0)
    const addTodoItem = (title) => {
        setTodoItems([...todoItems,{id:todoItems.length+1, title:title, is_done:false}])
    }
    const updateStatusTodoItem = (id) => {
        setTodoItems(todoItems.map(todoItem => {
            if (todoItem.id === id) {
                todoItem.is_done = !todoItem.is_done
            }
            return todoItem
        }))
    }
    const removeTodoItem = (id) => {
        setTodoItems(todoItems.filter(todoItem => todoItem.id !== id))
    }
    return (
        <div className="wrapper">
            <InputComponent addTodoItem={addTodoItem}/>
            <FilterComponent setFilterStatus={setFilterStatus}/>
            <ListComponent todoItems={todoItems} updateStatusTodoItem={updateStatusTodoItem} removeTodoItem={removeTodoItem} filterStatus={filterStatus}/>
        </div>
    )
}

▼App.css
.wrapper {
    width: 600px;
    margin: 0 auto;
}

▼InputComponent.js
import "./InputComponent.css"
import React, {useState} from 'react'
export default function InputComponent(props) {
    const [title, setTitle] = useState('')
    const changeTitle = (e) => {
        setTitle(e.target.value)
    }
    const save = (e) => {
        e.preventDefault()
        props.addTodoItem(title)
        setTitle('')
    }
    return (
        <form>
            <input type="text" placeholder="タイトル" value={title} onChange={changeTitle}/>
            <button onClick={save}>保存</button>
        </form>
    )
}

▼InputComponent.css
form {
    display: flex;
    justify-content: center;
    margin: 40px 0;
}

▼FilterComponent.js
import "./FilterComponent.css"
export default function FilterComponent(props) {
    const all = () => {
        props.setFilterStatus(0)
    }
    const done = () => {
        props.setFilterStatus(1)
    }
    const unDone = () => {
        props.setFilterStatus(2)
    }
    return (
        <div className="filters">
            <button onClick={all}>すべて</button>
            <button onClick={done}>完了</button>
            <button onClick={unDone}>未完了</button>
        </div>
    )
}

▼FilterComponent.css
.filters {
    display: flex;
    justify-content: center;
}
.filters button {
    margin: 10px;
}

▼ListComponent.js
import "./ListComponent.css"
export default function ListComponent(props) {
    return (
        <div className="todo-items">
            <ul>
                <li>
                    <span className="col-title-h">タイトル</span>
                    <span className="col-status-h">ステータス</span>
                    <span className="col-remove-h"/>
                </li>
                {props.todoItems.map(todoItem => {
                    if (props.filterStatus === 1 && !todoItem.is_done) {
                        return false
                    }
                    if (props.filterStatus === 2 && todoItem.is_done) {
                        return false
                    }
                    return (
                        <li key={todoItem.id}>
                            <span className="col-title">{todoItem.title}</span>
                            <span className="col-status">
                                <input type="checkbox"
                                       checked={todoItem.is_done}
                                       onChange={e => {
                                           props.updateStatusTodoItem(todoItem.id)
                                       }}
                                />
                            </span>
                            <span className="col-remove-h">
                                <button
                                    onClick={e => {
                                        e.preventDefault()
                                        props.removeTodoItem(todoItem.id)
                                    }}
                                >
                                    削除
                                </button>
                            </span>
                        </li>
                    )
                })}
            </ul>
        </div>
    )
}

▼ListComponent.css
.todo-items {
}
.todo-items ul {
    list-style: none;
    padding-left: 0;
}
.todo-items ul li {
    display: flex;
    border: solid 1px grey;
    margin: 5px 0;
    padding: 10px;
}
.col-title-h {
    width: 70%;
    text-align: center;
}
.col-status-h {
    width: 20%;
    text-align: center;
}
.col-remove-h {
    width: 10%;
    text-align: center;
}
.col-title {
    width: 70%;
}
.col-status {
    width: 20%;
    text-align: center;
}
.col-remove-h {
    width: 10%;
    text-align: center;
}
kyamashita_2022206_08.png

最後に

如何でしたでしょうか。

今回のサンプルで

Reactのコンポーネント分割、ステート、親子間の値の受け渡しと更新

このあたりの使い方の参考、最初のとっかかりになれば幸いです。

  • このエントリーをはてなブックマークに追加

この記事を読んだ人にオススメ