【まとめ】Next.js初心者がTODOアプリを作って引っかかった点の解説
初のNext.jsでのアプリ作成です。
自分用の備忘録&チームへの勉強過程の報告用として書きました。
初心者目線で実際に引っかかった点が詰まっているので、役に立つところがあるかと思います。
以下の記事を主に参考にさせていただきました。ありがとうございます。
【Next.js】定番のTodoアプリを作ってみる
NextjsとTypeScriptで簡単なTodoアプリを作ろう!
Reactのコンポーネント内で関数を呼び出すときの書き方の違い【onclick, onchange, アロー関数】
仕様
- Next.js + Tailwind CSS + shadcn/ui + Vercelを使う。
- 最小限の機能で作る。
- タスクを入力する欄がある。
- タスクを入力してエンターを押したら入力欄の下にリスト形式で表示させる。
- タスク一覧のアイテムは完了したら削除できるようにする。
- 完了済みのタスク一覧から完了済みのタスクを復元できるようにする。
完成物
Vercelで適当にデプロイ。
スマホサイズで作成したため、開発者モードを使ってスマホサイズで表示した方が見栄えが良いです。
(レスポンシブ対応はまた別の機会に実装予定)
コード
<Input>, <Button>, <Accordion>はshadcn/uiでインストールしたもの。
細かい設定なしでかっこいいデザインが作れるので非常に便利です。
shadcn/uiについては、大量に先人たちの記事が出てくるので説明は省きます。
コード全体
"use client";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
export const TodoList = () => {
const [text, setText] = useState<string>("");
const [todos, setTodos] = useState<{ sentence: string; visible: boolean }[]>(
[]
);
const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
const addTodo = () => {
if (text === "") {
console.log("空では追加できません");
return;
}
const newTodos = [...todos, { sentence: text, visible: true }];
setTodos(newTodos);
console.log(text + "を追加しました");
setText("");
};
const completeTodo = (index: number) => {
const newTodos = [...todos];
newTodos[index].visible = !newTodos[index].visible;
setTodos(newTodos);
console.log("完了しました");
};
return (
<div className="flex flex-col items-center justify-center text-center space-y-4 mt-5 mx-auto w-4/5 pd-8">
<Input
className=" bg-zinc-300/50"
placeholder="input TODO"
value={text}
onChange={changeText}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
e.preventDefault();
addTodo();
}
}}
/>
<Button className="w-full my-5 bg-zinc-500" onClick={addTodo}>
add
</Button>
{/* <button className="w-full my-5 bg-zinc-500/50" onClick={addTodo}>
追加
</button> */}
<div className="w-full container my-auto bg-zinc-300 rounded-sm">
<div className="w-full flex flex-col space-y-4 h-fit p-2 pb-4">
{todos.map(
(todo, index) =>
// todo.visibleがtrueである場合にのみ、括弧内のコード(つまりToDoアイテムの表示に関するコード)が実行されます。
todo.visible && (
<div
className="flex items-center mt-2 p-4 bg-white rounded-sm"
key={index}
>
<button
className="p-2 rounded-full"
// 引数を渡す場合はアロー関数で囲む必要がある
onClick={() => completeTodo(index)}
>
☑️
</button>
<p>{todo.sentence}</p>
</div>
)
)}
</div>
</div>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>完了済み</AccordionTrigger>
<AccordionContent>
<div className="w-full container my-auto bg-zinc-300 rounded-sm">
<div className="w-full flex flex-col space-y-4 h-fit p-2 pb-4">
{todos.map(
(todo, index) =>
todo.visible || (
<div
className="flex items-center mt-2 p-4 bg-white rounded-sm"
key={index}
>
<button
className="p-2 rounded-full"
// 引数を渡す場合はアロー関数で囲む必要がある
onClick={() => completeTodo(index)}
>
✅
</button>
<p>{todo.sentence}</p>
</div>
)
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};
"use client";
import Image from "next/image";
import { TodoList } from "@/components/TodoList";
export default function Home() {
return (
<main>
<h1 className="flex justify-center text-gray-800 font-bold text-2xl mt-6">
TODOリスト
</h1>
<TodoList></TodoList>
</main>
);
}
タスクの入力、表示、完了、復元の流れ
どこを説明しているかわかりやすいように、該当部分のコードを抜粋して下につける形で記載します。
<Input>に文字が入力される
↓
文字が変わるたびに、onChange={changeText}
が実行される
<Input
className=" bg-zinc-300/50"
placeholder="input TODO"
value={text}
onChange={changeText}
〜省略〜
↓
事前に定義したステート変数であるtext
に、changeText
関数の中でセッター関数であるsetText(e.target.value)
が実行されて、現在の文字列が代入される
export const TodoList = () => {
const [text, setText] = useState<string>("");
const [todos, setTodos] = useState<{ sentence: string; visible: boolean }[]>(
[]
);
const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
↓
<Input>のonKeyDown
もしくは <Button>のonClick
で addTodo
関数が実行される
↓
<Input
className=" bg-zinc-300/50"
placeholder="input TODO"
value={text}
onChange={changeText}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
e.preventDefault();
addTodo();
}
}}
/>
<Button className="w-full my-5 bg-zinc-500" onClick={addTodo}>
add
</Button>
↓ addTodo関数
内で以下の処理が実行される
- 文字列が空でないかの判定を行い、空の場合は関数を終了
- スプレッド構文を使って
newTodos
という名称で現在のtodos
配列のコピーを作成し、新しい要素をnewTodos
に追加- 新しい要素では、
sentence
をtext
(入力フォームの文字列)に、visible
を初期値としてtrue
に設定する
- 新しい要素では、
-
setTodos
関数を使用して、新しい要素が追加された配列をtodos
配列に上書き -
setText("")
を呼び出して、text
を空文字列にリセットすることで、入力フィールドをクリアする
const addTodo = () => {
if (text === "") {
console.log("空では追加できません");
return;
}
const newTodos = [...todos, { sentence: text, visible: true }];
setTodos(newTodos);
console.log(text + "を追加しました");
setText("");
};
↓
stateであるtodos
が更新されたので、再レンダリングが走る
↓
todos.map()
メソッドは、todos
配列の各要素に対して指定された関数を実行する
{todos.map(
(todo, index) =>
〜省略〜
↓
それぞれの要素に対して、論理AND演算子&&
を使ってtodo.visible
がtrue
かどうかの判定を行う
todo.visible && (
<div
className="flex items-center mt-2 p-4 bg-white rounded-sm"
key={index}
>
〜省略〜
↓
初期値でtodo.visible
はtrue
なので、<div>, <button>, <p>が表示される
todo.visible && (
<div
className="flex items-center mt-2 p-4 bg-white rounded-sm"
key={index}
>
<button
className="p-2 rounded-full"
// 引数を渡す場合はアロー関数で囲む必要がある
onClick={() => completeTodo(index)}
>
☑️
</button>
<p>{todo.sentence}</p>
</div>
〜省略〜
↓
<button>を押すとonClick={() => completeTodo(index)}
でcompleteTodo(index)
が実行される
↓
<button
className="p-2 rounded-full"
// 引数を渡す場合はアロー関数で囲む必要がある
onClick={() => completeTodo(index)}
>
☑️
</button>
↓
completeTodo(index)
ではクリックされた要素のvisible
の真偽値を、逆転させて上書きする。
const completeTodo = (index: number) => {
const newTodos = [...todos];
newTodos[index].visible = !newTodos[index].visible;
setTodos(newTodos);
console.log("完了しました");
};
↓
stateのnewTodos
が更新されたので再レンダリングされる
↓
shadcn/uiでインストールした<Accordion>の中に、タスク一覧と同様にmap
関数、論理OR演算子||
を使って完了済みタスク一覧を表示する
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>完了済み</AccordionTrigger>
<AccordionContent>
<div className="w-full container my-auto bg-zinc-300 rounded-sm">
<div className="w-full flex flex-col space-y-4 h-fit p-2 pb-4">
{todos.map(
(todo, index) =>
todo.visible || (
<div
className="flex items-center mt-2 p-4 bg-white rounded-sm"
key={index}
>
<button
className="p-2 rounded-full"
// 引数を渡す場合はアロー関数で囲む必要がある
onClick={() => completeTodo(index)}
>
✅
</button>
<p>{todo.sentence}</p>
</div>
)
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
引っかかった点
私と同様な初心者が引っかかりそうなポイントをまとめます。
GitHub Copilot 先生のおかげで自力でも解決できました。
JavaScript, TypeScript, React, Next.jsの関係は?
JavaScript
↓ スーパーセット(拡張版):静的型付けなどの機能を追加
TypeScript
↓ UIを提供する機能を追加
React
(→JavaScriptやTypeScriptのライブラリ)
↓ ルーティングやレンダリングの機能を追加
Next.js
(→Reactのフレームワーク)
React:ライブラリかフレームワークか?
ChatGPT4の回答
Reactは通常、「ライブラリ」として分類されます。なぜなら、Reactは主にUIの構築に焦点を当てており、アプリケーションの全体的なアーキテクチャを定義しません。Reactはコンポーネントを作成し、それらを組み合わせてUIを構築するためのツールを提供しますが、ルーティングや状態管理のような他の側面は開発者に委ねられています。これに対して、フレームワークはこれらの側面を統合的に管理し、より包括的なガイドラインと構造を提供します。
簡潔に言えば、React(ライブラリ)は「使いたい部分を選んで使う」ものであり、フレームワークは「全体のルールに従って開発する」ものと言えます。
冒頭の"use client";とは?
とてつもなく雑な表現をすると、useStateを使いたいファイルには記載が必要。
厳密に理解したい人向けの参考ページ集
実際にはuseStateを使いたい全てのファイルに記載するのではなく、reactツリー内のサーバーコンポーネントとクライアントコンポーネントの境界に一度だけ書くのが望ましい。
e.target.value
とは?
e.target.valueは「ユーザーが何かアクションを起こした場所で(target)、そのアクションによって生まれた具体的な情報(value)を取得する」という意味になります。
target:そのイベントが起きた場所や要素
value:具体的な値、ここでは<input>にユーザーが入力した値
onKeyDown
で、変換確定の際にEnterを押すと、誤って登録が実行されてしまう
e.isComposing
はIMEの入力状態が未確定かどうかを判別して、未確定の場合にtrue、違うならfalseを返す。
e.preventDefault();
はブラウザで標準の動作をブロックする。
inputフォームの標準動作は「actionで指定されたURLへ遷移+データ送信」である。
標準動作をブロックしたのちに自分が期待する動作の関数を実行すればよい。
画像は以下からお借りしました。
以上2つを組み合わせて以下の形にすると、変換が確定した状態でEnterを押した場合のみ、addTodo
が実行され、タスクが登録される。
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
e.preventDefault();
addTodo();
}
}}
onClick内で引数を渡す場合はアロー関数で囲む
結論から書くと、onClick={addTodo}
は正しいが、onClick={completeTodo(index)}
は誤り。また、引数がある場合は基本的にアロー関数を使う。
(なお、アロー関数を使わずに書く方法もある。)
typescriptで関数を呼ぶときは以下のパターンがある。
①「関数名」
②「関数名( )」
③「( ) => 関数名 ( )」
①「関数名」はイベントによって実行したい関数を参照しているだけである。そのため、onClick={addTodo}
はクリックされたときに実行される。
JavaScriptでは、②のように関数名の後に括弧()を付けると、その関数は即時に実行される。したがって、onClick={completeTodo(index)}
と書くと、completeTodo(index)
関数はレンダリング時に即時実行されてしまう。
つまり、今回はクリックされてから実行したいので、アロー関数で③「( ) => 関数名 ( )」の書き方にする必要がある。
【補足】 onClick={addTodo}はonClick={() => addTodo()}と書いても問題ありませんか?
GitHub Copilotの回答
はい、onClick={addTodo}とonClick={() => addTodo()}は同じ動作をしますが、微妙な違いがあります。
onClick={addTodo}は、クリックイベントが発生したときにaddTodo関数を直接呼び出します。この場合、addTodo関数はクリックイベントオブジェクトを引数として受け取ります。
一方、onClick={() => addTodo()}は、クリックイベントが発生したときに新しい無名関数(アロー関数)を呼び出し、その無名関数がaddTodo関数を呼び出します。この場合、addTodo関数は引数を受け取りません。
したがって、addTodo関数がイベントオブジェクトを必要としない場合、どちらの書き方でも問題ありません。しかし、addTodo関数がイベントオブジェクトを必要とする場合、onClick={addTodo}を使用する必要があります。
&&
と||
はなぜ条件分岐に使えるのか?
react公式ページによると、そういう仕様らしい。
頭がPython基準なので、ANDとORは真偽値の評価にしか使えないと思っていた。
これが動作するのは、JavaScript では true && expression は必ず expression と評価され、false && expression は必ず false と評価されるからです。
従って、条件部分が true であれば、&& の後に書かれた要素が出力に現れます。もし false であれば、React はそれを無視して飛ばします
Stateとは?
とてつもなく雑な認識だと、いわゆる「変数」のようなもの。
今まではよくわからないものだと思っていたが、ちょっと扱いが面倒なただの変数だと思うと腑に落ちた。
変数との大きな違いとして
①Stateが変更されるとコンポーネントが再レンダリングされる
②setTextなどの専用の関数を使って値を更新する
// Stateの定義
// ステート変数とセッター関数を同時に定義する
const [text, setText] = useState<string>("Hello");
// 値を更新
// 直接代入できないので、専用のセッター関数を使う
setText("Goodbye");
// 変数の定義
let text: string = "Hello";
// 値の更新
// 直接代入できる
text = "Goodbye";
Discussion