はじめに
こんにちは!
Reactを学び始めてもうすぐ3週間です!
今回はViteで初期セットアップをして
ReactでTodoアプリを作ったのでアウトプットしていきます!
以降はGitHubアカウント、リポジトリがある前提で話を進めます。
初期セットアップ
以下のコマンドを叩いて初期セットアップをしていきます
npm create vite@latest
詳細は私が書いた以下の記事にあるのでご覧ください
デザイン
今回は初期セットアップのデフォルトで記載されているCSSファイルとTailwind CSSにてスタイルを当てていきます。
初期セットアップが終了したら以下のコマンドでTailwind CSSをインストールしていきます。
npm install tailwindcss @tailwindcss/vite
インストールが完了したら、vite.config.js
に以下のコードを記述します。
import { defineConfig } from 'vite'
+import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
base: '/react_todo_list',
plugins: [
react(),
+ tailwindcss(),
],
})
今回のTodoアプリの仕様
Create
- 入力フィールドと追加のボタンがある
- 入力フィールドに「仕事」のような文字を入力して追加ボタンを押すとTodo一覧に表示される
Read
- Todo一覧が表示されている
- 仕事(編集)(削除)のように表示される
- 編集と削除はボタン
- チェックボックスでタスクの完了と未完了がわかる
-
全てのタスク:3 完了済み:2 未完了:1
上記ようにタスクの数を表示する - チェックボックスにチェックをいれて
タスクを完了させることによりタスクの数はリアルタイムで変わる - タスクを削除するとタスクの数は減り、新規作成するとタスクの数は増える
Update
- 編集ボタンを押すと文字がフォームに変わってその場で編集できる
- [仕事](戻る)(更新)と表示され保存すると変更した文字にタイトルが変わる
- 任意のタスクのタイトル変更時、別のタスクの状態を変更することはできない
Delete
削除ボタンを押すと「本当によろしいですか?」と表示されOKを押すと削除できる
完成イメージ画像
実装部分
このセクションは汚いコードの作成から始まります。
最終的なコードがどうなったか知りたい方はコンポーネント+リファクタリングへ
以下の順序で書いていきます。
- Create
- Read
- Update
- Delete
- コンポーネント化
- 初期セットアップ完了後のSVG等の不要なコード、細かいCSSは省略して書いていきます
Create
状態管理
const [input, setInput] = useState('');
const nextId = useRef(1);
処理関数
const addTodoText = (event) => setInput(event.target.value);
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: nextId.current++,
text: input,
completed: false,
}])
setInput('');
}
}
UI部分
<div className='flex mb-8 gap-x-2'>
<input type="text" className='flex-1 border-2 border-white rounded-xl p-2' value={input} onChange={addTodoText} name='todo-input' />
<button onClick={addTodo} className='bg-blue-500'>追加</button>
</div>
Read
状態管理
const [todos, setTodos] = useState([]);
統計関数
const completedTodos = (todos) => todos.filter(todo => todo.completed).length;
const incompletedTodos = (todos) => todos.filter(todo => !todo.completed).length;
UI部分(通常モード)
<h1 className='mb-8'>Todo App</h1>
<div className='mb-8'>
<p>全てのタスク: {todos.length} / 完了済み: {completedTodos(todos)} / 未完了: {incompletedTodos(todos)}</p>
</div>
{/* リスト表示部分 */}
<div>
<ul>
{todos.map((todo) => {
<li key={todo.id} className={`grid grid-cols-12 items-center gap-x-2 mb-4 ${isEditing ? 'opacity-50' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className='col-span-1'
disabled={isEditing}
/>
<p className={`col-span-5 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
{todo.text}
</p>
<button
onClick={() => editTodo(todo)}
className='col-span-3 bg-zinc-600'
disabled={isEditing}
>
編集
</button>
<button
onClick={() => setTodos(todos.filter((t) => t.id !== todo.id))}
className='col-span-3 bg-red-600'
disabled={isEditing}
>
削除
</button>
</li>
})}
</ul>
</div>
Update
状態管理
const [isEditing, setIsEditing] = useState(false);
const [currentTodo, setCurrentTodo] = useState(null);
処理関数
const editTodo = (todo) => {
setIsEditing(true);
setCurrentTodo({...todo});
}
const cancelEdit = () => {
setIsEditing(false);
setCurrentTodo(null);
};
const updateTodos = (todo) => {
if (currentTodo && currentTodo.text.trim()) {
setTodos(todos.map(todo =>
todo.id === currentTodo.id ? currentTodo : todo
));
setIsEditing(false);
setCurrentTodo(null);
}
}
const toggleTodo = (todoId) => {
setTodos(prevTodos =>
prevTodos.map(t =>
t.id === todoId ? { ...t, completed: !t.completed } : t
)
);
}
UI部分(編集モード)
{todos.map((todo) => {
if (isEditing && currentTodo && currentTodo.id === todo.id) {
return (
<li key={todo.id} className='grid grid-cols-12 items-center gap-x-2 mb-4'>
<input
className='col-span-6 border-2 border-white rounded-xl p-2'
value={currentTodo.text}
onChange={(e) => setCurrentTodo({...currentTodo, text: e.target.value})}
/>
<button onClick={() => cancelEdit()} className='col-span-3 bg-zinc-600'>戻る</button>
<button onClick={() => updateTodos()} className='col-span-3 bg-emerald-600'>更新</button>
</li>
);
}
// 通常モードは省略
})}
Delete
処理関数
const deleteTodo = (todoId) => {
const message = '本当によろしいですか?';
if (confirm(message)) {
setTodos(todos.filter(todo => todo.id !== todoId));
}
}
UI部分
// ...
<button
onClick={() => deleteTodo(todo.id)}
className='col-span-3 bg-red-600'
disabled={isEditing}
>
削除
</button>
// ...
殴り書きの結果
import { useRef, useState } from 'react'
import './App.css'
function App() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [currentTodo, setCurrentTodo] = useState(null);
const nextId = useRef(1);
const addTodoText = (event) => setInput(event.target.value);
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: nextId.current++,
text: input,
completed: false,
}])
setInput('');
}
}
const toggleTodo = (todoId) => {
setTodos(prevTodos =>
prevTodos.map(t =>
t.id === todoId ? { ...t, completed: !t.completed } : t
)
);
}
const completedTodos = (todos) => todos.filter(todo => todo.completed).length;
const incompletedTodos = (todos) => todos.filter(todo => !todo.completed).length;
const editTodo = (todo) => {
setIsEditing(true);
setCurrentTodo({...todo});
}
const cancelEdit = () => {
setIsEditing(false);
setCurrentTodo(null);
};
const updateTodos = (todo) => {
if (currentTodo && currentTodo.text.trim()) {
setTodos(todos.map(todo =>
todo.id === currentTodo.id ? currentTodo : todo
));
setIsEditing(false);
setCurrentTodo(null);
}
}
const deleteTodo = (todoId) => {
const message = '本当によろしいですか?';
if (confirm(message)) {
setTodos(todos.filter(todo => todo.id !== todoId));
}
}
return (
<>
<h1 className='mb-8'>Todo App</h1>
<div className='flex mb-8 gap-x-2'>
<input type="text" className='flex-1 border-2 border-white rounded-xl p-2' value={input} onChange={addTodoText} name='todo-input' />
<button onClick={addTodo} className='bg-blue-500'>追加</button>
</div>
<div className='mb-8'>
<p>全てのタスク: {todos.length} / 完了済み: {completedTodos(todos)} / 未完了: {incompletedTodos(todos)}</p>
</div>
<div>
<ul>
{todos.map((todo) => {
if (isEditing && currentTodo && currentTodo.id === todo.id) {
return (
<li key={todo.id} className='grid grid-cols-12 items-center gap-x-2 mb-4'>
<input
className='col-span-6 border-2 border-white rounded-xl p-2'
value={currentTodo.text}
onChange={(e) => setCurrentTodo({...currentTodo, text: e.target.value})}
/>
<button onClick={() => cancelEdit()} className='col-span-3 bg-zinc-600'>戻る</button>
<button onClick={() => updateTodos()} className='col-span-3 bg-emerald-600'>更新</button>
</li>
);
}
return (
<li key={todo.id} className={`grid grid-cols-12 items-center gap-x-2 mb-4 ${isEditing ? 'opacity-50' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className='col-span-1'
disabled={isEditing}
/>
<p className={`col-span-5 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
{todo.text}
</p>
<button
onClick={() => editTodo(todo)}
className='col-span-3 bg-zinc-600'
disabled={isEditing}
>
編集
</button>
<button
onClick={() => deleteTodo(todo.id)}
className='col-span-3 bg-red-600'
disabled={isEditing}
>
削除
</button>
</li>
);
})}
</ul>
</div>
</>
)
}
export default App
いや〜、これぞ殴り書き!ですね。
次からコンポーネントに分割していきますよ。
最終的に App.jsx は殴り書きの半分くらいの100行程度になります。
コンポーネント+リファクタリング
以下のコンポーネントを作成とリファクタリングをしていきます。
今回は小規模な実装な為、components
フォルダを作成して src/components
配下に全てのコンポーネント作成します。
リファクタリングでは以下のことをします。
- タスク状況の統計関数を定数化
- addTodoで早期エラーリターン
- mapを使用する際、keyが漏れている部分があるため追記
- 編集中を判定する isEditing の削除
- ES6のオブジェクトプロパティ省略記法
TodoItem
import { EditingTodo } from './EditingTodo';
import { CompletedTodo } from './CompletedTodo';
import { EditButton } from './EditButton';
import { DeleteButton } from './DeleteButton';
export const TodoItem = (props) => {
const { todo, editProps, todoActions } = props;
const edit = editProps.currentTodo && editProps.currentTodo.id === todo.id;
return (
<>
{edit ?
<EditingTodo
key={todo.id}
currentTodo={editProps.currentTodo.text}
onEditTodo={editProps.onEditTodo}
cancelEdit={editProps.cancelEdit}
updateTodos={editProps.updateTodos}
/>
:
<li key={todo.id} className={`grid grid-cols-12 items-center gap-x-2 mb-4 ${editProps.currentTodo ? 'opacity-50' : ''}`}>
<CompletedTodo
isCompleted={todo.completed}
toggleTodo={todoActions.toggleTodo}
isEditing={editProps.currentTodo}
todoTitle={todo.text}
/>
<EditButton
onClickEdit={todoActions.onClickEdit}
isEditing={editProps.currentTodo}
/>
<DeleteButton
onClickDelete={todoActions.onClickDelete}
isEditing={editProps.currentTodo}
/>
</li>
}
</>
);
}
InputTodo
export const InputTodo = (props) => {
const { todoText, onChangeTodoText, addTodo } = props;
return (
<div className='flex mb-8 gap-x-2'>
<input
type="text"
className='flex-1 border-2 border-white rounded-xl p-2'
value={todoText}
onChange={onChangeTodoText}
name='todo-input'
placeholder="タスクを追加..."
/>
<button onClick={addTodo} className='bg-blue-500'>追加</button>
</div>
);
}
EditingTodo
export const EditingTodo = (props) => {
const { currentTodo, onEditTodo, cancelEdit, updateTodos} = props;
return (
<li key={currentTodo.id} className='grid grid-cols-12 items-center gap-x-2 mb-4'>
<input
className='col-span-6 border-2 border-white rounded-xl p-2'
value={currentTodo}
onChange={onEditTodo}
/>
<button onClick={cancelEdit} className='col-span-3 bg-zinc-600'>戻る</button>
<button onClick={updateTodos} className='col-span-3 bg-emerald-600'>更新</button>
</li>
);
}
EditButton
export const EditButton = (props) => {
const { onClickEdit, isEditing } = props;
return (
<button
onClick={onClickEdit}
className='col-span-3 bg-zinc-600'
disabled={isEditing}
>
編集
</button>
);
}
DeleteButton
export const DeleteButton = (props) => {
const { onClickDelete, isEditing } = props;
return (
<button
onClick={onClickDelete}
className='col-span-3 bg-red-600'
disabled={isEditing}
>
削除
</button>
);
}
CompletedTodo
export const CompletedTodo = (props) => {
const { isCompleted, toggleTodo, isEditing, todoTitle } = props;
return (
<>
<input
type="checkbox"
checked={isCompleted}
onChange={toggleTodo}
className='col-span-1'
disabled={isEditing}
/>
<p className={`col-span-5 ${isCompleted ? 'line-through text-gray-500' : ''}`}>
{todoTitle}
</p>
</>
);
}
App.jsx
import { useRef, useState } from 'react'
import './App.css'
import { InputTodo } from './components/InputTodo';
import { TodoItem } from './components/TodoItem';
function App() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const [currentTodo, setCurrentTodo] = useState(null);
const nextId = useRef(1);
const addTodoText = (event) => setInput(event.target.value);
const addTodo = () => {
const text = input.trim();
if (!text) return;
setTodos([...todos, {
id: nextId.current++,
text: input,
completed: false,
}])
setInput('');
}
const toggleTodo = (todoId) => {
setTodos(prevTodos =>
prevTodos.map(t =>
t.id === todoId ? { ...t, completed: !t.completed } : t
)
);
}
const completedTodos = todos.filter(todo => todo.completed).length;
const incompletedTodos = todos.filter(todo => !todo.completed).length;
const onEditTodo = (e) => {
setCurrentTodo({...currentTodo, text: e.target.value});
}
const cancelEdit = () => setCurrentTodo(null);
const updateTodos = () => {
if (currentTodo && currentTodo.text.trim()) {
setTodos(todos.map(todo =>
todo.id === currentTodo.id ? currentTodo : todo
));
setCurrentTodo(null);
}
}
const editTodo = (todo) => setCurrentTodo({...todo});
const deleteTodo = (todoId) => {
const message = '本当によろしいですか?';
if (confirm(message)) {
setTodos(todos.filter(todo => todo.id !== todoId));
}
}
return (
<>
<h1 className='mb-8'>Todo App</h1>
<InputTodo
todoText={input}
onChangeTodoText={addTodoText}
addTodo={addTodo}
/>
<div className='mb-8'>
<p>全てのタスク: {todos.length} / 完了済み: {completedTodos} / 未完了: {incompletedTodos}</p>
</div>
<div>
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
editProps={{
currentTodo,
onEditTodo,
cancelEdit,
updateTodos,
}}
todoActions={{
toggleTodo: () => toggleTodo(todo.id),
onClickEdit : () => editTodo(todo),
onClickDelete: () => deleteTodo(todo.id),
}}
/>
))}
</ul>
</div>
</>
)
}
export default App
かなりスッキリました!
コンポーネントすることでコードの管理が楽になったことを実感しました。
感想
初めは App.jsx へ殴り書きをして
動くものができたらコンポーネントに分割するようにしました。
1つのタスクをコンポーネントとして分割する際に props が11個まで膨れ上がった時がありました!ちょっとだけ「Reactって非効率じゃないかな?」って思いましたが、進めていくうちに最終的に3つまで減らすことができました!
編集中を判定する実装では、isEditingが必須だなと感じてすぐにuseStateを定義したのですが、最終的にはいらなくなりました。何でもuseStateすればいいという話では無いですね...
JSX記法では、if文より三項演算子の方がつかい機会が多いなと調べていて感じました。こればかりは現場によって違う可能性もあるので実務に入ってから力をつけようと思います!
参考文献
https://ja.vite.dev/guide/
https://tailwindcss.com/docs/installation/using-vite