【React】シンプルな「TODOアプリ」を作ってみる
こんにちわ。kyamashitaです。
「React」の連載第三弾ということで、シンプルなTODOアプリを実際に作ってみたいと思います。
完成イメージ
ざっとこんな感じのイメージで作ってみます。
コンポーネントは「入力用」「フィルター用」「一覧表示用」の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>
)
}
このように表示されると思います。
要素のコーディング
各コンポーネントに要素をコーディングします。
▼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>
)
}
ひとまずこのようになると思います。
データを保持して一覧で表示する
ステートフックと副作用フックを使用します。
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>
)
}
入力した内容を保存できるようにする
まずは保存ボタンを押した時にタイトルを取得してみます。
▼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;
}
最後に
如何でしたでしょうか。
今回のサンプルで
Reactのコンポーネント分割、ステート、親子間の値の受け渡しと更新
このあたりの使い方の参考、最初のとっかかりになれば幸いです。