1. 前提・仕様
1-2. から本文。
1-1. 自己紹介・前提
これまでの経緯として、アプリ開発などを授業で行ってきたが、分からない部分をバイブコーディングで行ってきたこと・継続的に学習とアウトプットを行ってこなかったことにより、実力がないことを実感している。
これを解消して1人前(食べていけるくらい)のエンジニアになるため、これからアウトプット中心の学習を行い、実践重視の学習を行う。
現在のロードマップ
1. 自己学習+個人開発である程度開発のノウハウ・実績を作る
2. アルバイト雇用でも良いのでチーム開発のノウハウを身に着ける(少人数でインフラなどあらゆる領域に携われたらなお良し)
3. 個人開発を継続的に行い、少人数でも継続的に利用されるサービスを作る
4. 就職 or フリーランス これは、就職活動を行い、ありがたいことに内定をいただけたら、また個人の活動に戻るという方向性に一旦しておく。
学習に関する悩み
- 最適な学習方法を模索中
- これからのことを考えたらReact => TypeScript => Next.js・Docker・Cloud(AWS,GCP)・terraformなど学ぶべきことがたくさんあると感じているが、本当に必要な能力がどれで、どこまでか判断できない。
以下のQuita記事のロードマップに従い、課題1に取り組んだ。
1-2. インプット
以下の教材を1周した。
1-3. 仕様
Reactを用いて以下の仕様でアプリを作る。
作成するもの
エンジニアとしての学習時間は1000時間が必要と言われています
そこで日々の学習の内容と学習時間を記録するアプリを作ることにしました
以下の実装をすべて行ってください
ただしCSSなどのスタイリングは不要です
-
Viteを利用してReact環境を用意する
- Node.jsをインストールする
- Hello Worldが表示される
- タイトル「学習記録一覧」を見ることができる
- テストデータを一覧で表示する
const records = [
{ title: "勉強の記録1", time: 1},
{ title: "勉強の記録2", time: 3},
{ title: "勉強の記録3", time: 5}
]
- 学習内容の入力フォームをみることができる
- 学習時間の入力フォームをみることができる
- 学習時間の入力フォームは数字を入力できる
- 登録ボタンをみることができる
- 登録ボタンをクリックするとrecordsに記録を追加できる
- 登録をしたらフォームが初期化される
- 全項目が入力されていないときにエラーが表示される
- 正しく入力されている場合登録ボタンを押すとエラーが消える
- 記録した勉強の時間を合計した値をみることができる
2. 実装の流れ
インプットで学習した内容と仕様をもとにアプリ作成の流れをまとめていく。
2-1. React環境の準備
以下のコマンドでReactを用いたViteプロジェクトを作成する。
npm create vite@latest
互換性について
Vite は Node.js 20.19+, 22.12+ のバージョンが必要です。ただし、一部のテンプレートではそれ以上のバージョンの Node.js を必要としますので、パッケージマネージャーが警告を出した場合はアップグレードしてください。
-
Viteを利用してReact環境を用意する
- Node.jsをインストールする
- Hello Worldが表示される
2-2. 必要な要素(骨組み)を作る
App.jsxの実装
export const App =() => {
return (
<>
<div>
<p>学習内容</p>
<input type="text"/>
<p>学習時間</p>
<input type="text"/>
<p>時間</p>
</div>
<div>
<p>入力されている学習内容:</p>
<p>入力されている時間:</p>
<button>登録</button>
</div>
<div>
<ul>
<li>
<div>
<p>テスト:1</p>
</div>
</li>
</ul>
<p>入力されていない項目があります</p>
</div>
<div>
<p>合計時間: / 1000(h)</p>
</div>
</>
);
};
画面の表示
- 学習内容の入力フォームをみることができる
- 学習時間の入力フォームをみることができる
- 学習時間の入力フォームは数字を入力できる
- 登録ボタンをみることができる
2-3. テストデータでリストを表示する
App.jsxの実装
export const App =() => {
const records = [
{ title: "勉強の記録1", time: 1},
{ title: "勉強の記録2", time: 3},
{ title: "勉強の記録3", time: 5}
]
return (
<>
<div>
<p>学習内容</p>
<input type="text"/>
<p>学習時間</p>
<input type="text"/>
<p>時間</p>
</div>
<div>
<p>入力されている学習内容:</p>
<p>入力されている時間:</p>
<button>登録</button>
</div>
<div>
<ul>
{records.map((record) => (
<li key={record.title}>
<div>
<p>{record.title}:{record.time}</p>
</div>
</li>
))}
</ul>
<p>入力されていない項目があります</p>
</div>
<div>
<p>合計時間: / 1000(h)</p>
</div>
</>
);
};
画面の表示
map関数でkeyを指定しなければいけない理由
Reactがリストのどの要素が変更・追加・削除されたかを正確にかつ効率的に把握するため
Reactは、画面の描画を高速化するため、仮想DOMという仕組みを使用している。リストに変更があった場合、Reactは古いリストと新しいリストを見比べて、変更があった部分だけを実際の画面(DOM)に反映させる。
keyがないとリストの途中に新しい項目が追加されたり、削除されたり、並び替えたりした時、予期せぬバグや、全体の再描画によるパフォーマンスの低下を起こす。
*だから本当はもっとリスト内で一意の被らない値にするべき
アロー関数の処理が () で返される理由
アロー関数では、処理を波括弧 {} で囲むか、丸括弧 () で囲むかで、返り値の扱いが変わり、 () は省略記法
{} を使うと「ブロック」として扱われるため、関数の中で複数処理を書け、明示的に return と書く必要がある。
() を使うとその中身が自動的にそのまま return される。
return を省略してJSXをそのまま返すための簡略化された書き方
- テストデータを一覧で表示する
2-4. 登録ボタンを押すとRecordsに記録が追加される
import { useState } from "react";
export const App =() => {
const [titleText, setTitleText] = useState('');
const [timeText, setTimeText] = useState('');
const [records, setRecords] = useState([
{ title: "勉強の記録1", time: 1},
{ title: "勉強の記録2", time: 3},
{ title: "勉強の記録3", time: 5}
]);
const onChangeTitleText = (event) => setTitleText(event.target.value);
const onChangeTimeText = (event) => setTimeText(event.target.value);
const onClickRegister = () => {
const newRecords = [...records, {title: titleText, time: Number(timeText)}];
setRecords(newRecords);
}
return (
<>
<div>
<p>学習内容</p>
<input type="text" value={titleText} onChange={onChangeTitleText}/>
<p>学習時間</p>
<input type="text" value={timeText} onChange={onChangeTimeText}/>
<p>時間</p>
</div>
<div>
<p>入力されている学習内容:{titleText}</p>
<p>入力されている時間:{timeText}</p>
<button onClick={onClickRegister}>登録</button>
</div>
<div>
<ul>
{records.map((record) => (
<li key={record.title}>
<div>
<p>{record.title}:{record.time}</p>
</div>
</li>
))}
</ul>
<p>入力されていない項目があります</p>
</div>
<div>
<p>合計時間: / 1000(h)</p>
</div>
</>
);
};
画面の表示
useStateフック
-
入力値の保持(useStateの活用)
titleTextとtimeTextという状態(State)を作成し、ユーザの入力したテキストデータをReact側で記憶できるようにしている -
リアルタイムな反映(onChangeイベント)
入力の文字が変更されるたびに関数が実行され、Stateが更新される。これにより p タグの{titleText}の部分に、打った文字がリアルタイムで反映される -
データの追加
「登録」ボタンを押すと、既存の記録(records)を展開し、新しく入力されたデータを追加して、setRecordsでリスト全体を上書き更新する
- 登録ボタンをクリックするとrecordsに記録を追加できる
2-5. その他要件
import { useState } from "react";
export const App =() => {
const [titleText, setTitleText] = useState('');
const [timeText, setTimeText] = useState('');
const [totalTime, setTotalTime] = useState(0);
const [records, setRecords] = useState([]);
const [isIncompleteText, setIsIncompleteText] = useState(false);
const onChangeTitleText = () => setTitleText(event.target.value);
const onChangeTimeText = () => setTimeText(event.target.value);
const onClickRegister = () => {
if (titleText === "") {
setIsIncompleteText(true); return;
}
setIsIncompleteText(false)
const newRecords = [...records, {title: titleText, time: Number(timeText)}];
setRecords(newRecords);
const newTotalTime = totalTime + Number(timeText); setTotalTime(newTotalTime);
setTimeText('');
setTitleText('');
}
return (
<>
<div>
<p>学習内容</p>
<input type="text" value={titleText} onChange={onChangeTitleText}/>
<p>学習時間</p>
<input type="text" value={timeText} onChange={onChangeTimeText}/>
<p>時間</p>
</div>
<div>
<p>入力されている学習内容:{titleText}</p>
<p>入力されている時間:{timeText}</p>
<button onClick={onClickRegister}>登録</button>
</div>
<div>
<ul>
{records.map((record) => (
<li key={record.title}>
<div>
<p>{record.title}:{record.time}</p>
</div> </li>
))}
</ul>
{isIncompleteText && ( <p>入力されていない項目があります</p>) }
</div>
<div>
<p>合計時間:{totalTime} / 1000(h)</p>
</div>
</>
);
};
画面の表示
主な変更ポイント
-
初期データのクリア
ダミーデータから空の配列に変更 -
未入力のチェック
新しいStateを追加し、登録ボタンを押した際に学習内容が空欄だったらエラー状態を返す(timeTextも見た方が良かった、、、)また、画面の表示は && により前項がfalsyだったら後項は表示されず、truthyだったら表示される。 - 合計時間の自動計算と表示
- 登録後の入力欄リセット
- 登録をしたらフォームが初期化される
- 全項目が入力されていないときにエラーが表示される
- 正しく入力されている場合登録ボタンを押すとエラーが消える
- 記録した勉強の時間を合計した値をみることができる
3. 振り返り(AIとの壁打ち)
⚠️ 修正が必要なポイント(エラーの原因)
非常に素晴らしいロジックが組めていますが、1点だけこのまま実行すると ReferenceError: event is not defined というエラーで画面が真っ白になってしまう箇所があります。
修正箇所:
onChange イベントの関数で、引数として event を受け取る記述が抜けてしまっています。以下のように (event) を追加してください。
// 変更前(現在のコード)
const onChangeTitleText = () => setTitleText(event.target.value);
const onChangeTimeText = () => setTimeText(event.target.value);
// 変更後(修正版)
const onChangeTitleText = (event) => setTitleText(event.target.value);
const onChangeTimeText = (event) => setTimeText(event.target.value);
なぜ (event) と明記して修正すべきなのか?
「たまたまブラウザが気を利かせて動かしてくれている状態」から、「Reactのルールに則った、どの環境でも絶対に壊れない安全なコード」にするために、引数として (event) を受け取る記述を追加しておくのがベストプラクティスです。
Stateの削減(Derived Stateの活用)
現在、totalTime を useState で管理し、登録のたびに計算して更新していますね。実はReactのベストプラクティスでは、「他のデータから計算できる値は、Stateとして持たない(Derived State)」という鉄則があります。
records の配列の中にすべての時間データが入っているため、totalTime は records から毎回計算するだけで済みます。
// 変更前:Stateとして管理している
const [totalTime, setTotalTime] = useState(0);
// 変更後:Stateをやめて、recordsから直接計算する
const totalTime = records.reduce((sum, record) => sum + record.time, 0);
キー(key)の重複対策
現在、リストの表示で key={record.title} としています。もしユーザーが「英語」という学習内容を2回登録した場合、同じ key が2つ存在することになり、Reactがエラーを出したり、画面の描画がおかしくなったりします。
これを防ぐため、登録時に絶対に被らない「一意のID」を持たせるのが定石です。
// 登録時に crypto.randomUUID() でユニークなIDを発行する
const newRecords = [
...records,
{ id: crypto.randomUUID(), title: titleText, time: Number(timeText) }
];
// 表示するときは、そのIDをキーにする
<li key={record.id}>
想定外の入力(異常値)のブロック
現在のコードでは、ユーザーが「時間」の入力欄に 「あいう」 などの文字を入れるとどうなるでしょうか?
Number("あいう") は NaN(Not a Number:数値ではない)になってしまい、合計時間が「NaN / 1000」と表示され、アプリが壊れてしまいます。
これを防ぐには、HTML側で工夫するか、JavaScript側でバリデーションを強化します。一番簡単なのは の type を変えることです。
// type="number" にすることで、スマホなら数字キーボードが開き、文字の入力を防げます。
// min="0" でマイナスの時間も防げます。
<p>学習時間</p>
<input type="number" min="0" value={timeText} onChange={onChangeTimeText}/>
あわせて、onClickRegister の中でも「時間が入っていない場合」のエラーチェックを追加するとさらに完璧です。
4.まとめ
コメント
Udemyで学習しながらハンズオンで進めたアプリを作った直後だと、記憶も真新しいので、そこまで苦しむことなく作れた。また、分からなくなっても、作ったコードを見返しながらすれば問題なくできた。
学び
生成AIに要件を投げれば動くアプリは作れる。しかし、コードの書き方などは人(人が作った動画など)から学ぶ方が効率良く学べると感じた。同じ要件でも作り方は何通りもあるので、その中から実務で使われやすい書き方・ベストプラクティスのようなものを、より高い精度で生成AIから引き出す方法を見つけたいと感じた。(もしかしたらそんなの考えるよりも早く実務に関わるのが一番かも)




