副業に向けて React を学び、ポートフォリオ用に 案件管理ツール(CaseManagementTool) を Vite + TypeScript で開発しています。
前回は検索機能まで実装しました。今回は⑥ データコンポーネントと案件の追加・削除・更新 に向けて、UI 改善・コンポーネント分割・state 管理・つまずきを実装しながら整理したメモです。
スタック: React 19 / TypeScript / Vite
読者想定: React 初級〜中級
前回: 検索(Search / DataFilter / AllData と Data の分離)
この記事でやること
| 項目 | 内容 |
|---|---|
| UI | CSS で業務ツール風の見た目に整える |
| 追加 |
InsertData コンポーネント + utils/InsertData.ts
|
| 削除 |
DataTable の削除ボタン + utils/DeleteData.ts
|
| 更新 | 行内編集(案1)+ draftData / ChangeFlag
|
| 設計 | なぜ state を App に集約するか |
プロジェクトの全体像(現時点)
ディレクトリ構成
src/
App.tsx
App.css
index.css
types/Types.ts
components/
Search.tsx
DataTable.tsx
InsertData.tsx
utils/
DataFilter.ts
InsertData.ts
DeleteData.ts
ChangeDataFlag.ts
Index.ts
コンポーネント構成
App(state を持つ)
├── InsertData(案件登録フォーム)
├── Search(検索)
└── DataTable(一覧 + 変更 + 削除)
state の役割
| state | 役割 |
|---|---|
AllData |
全案件データ(マスタ) |
Data |
一覧表示用(検索後の結果もここ) |
SearchItems |
検索条件 |
draftData |
編集中の仮データ(確定前) |
機能実装前に UI を整えた
考えていたこと
機能を足す前に、見た目がモチベーションに影響する。CSS で先に整えよう。
方針
-
index.css… 配色・ベーススタイル -
App.css… ヘッダー、パネル、ボタン、テーブル - 共通クラス
panel/btn btn-primaryで統一
学んだこと: 見た目を先に整えると、フォームやボタンを追加するときも CSS の型が使える。
案件追加コンポーネント InsertData
考えていたこと
前回決めた構成に、案件追加用コンポーネントを足す。
変更ファイル一覧
-
新規:
src/components/InsertData.tsx -
新規:
src/utils/InsertData.ts -
加筆:
src/types/Types.ts(InsertProps) -
加筆:
src/App.tsx(onAddData)
登録の流れ
InsertData(入力)
→ onAddData(App)
→ AllData / Data に追加
const onAddData = (newAddData: Types.Data) => {
if (newAddData == null) return;
const newData = [...AllData, newAddData];
SetAllData(newData);
SetData(newData);
};
<InsertData onAddData={onAddData} />
utils にロジックを分離
考えていたこと(備忘録)
追加機能はあっさり書けたが、手癖でコンポーネント内に直書きしてしまい、後から utils に分離した。
| 関数 | 役割 |
|---|---|
ChangeInsertData |
フォーム入力を Data 型に反映 |
AddData |
配列末尾に追加 |
toDateInputValue |
Date → YYYY-MM-DD(date input 用) |
学んだこと: DataFilter と同様、表示はコンポーネント・ロジックは utils に分けると後から楽。
Q&A: フォームと Date 型
考えていたこと
期限 input の value に Date をそのまま渡したら型エラーになった。
問: value={NewData.Deadline} で「型 'Date' を型 'string | number | readonly string[]' に割り当てることはできません」と出る。どう直す?
解答:
-
<input>のvalueは 文字列(または number) -
Types.Data.DeadlineはDate -
フォームでは文字列、ドメイン型では
Dateと分ける
<input
type="date"
value={NewData?.Deadline ? Utils.toDateInputValue(NewData.Deadline) : ""}
onChange={(E) => setNewData(Utils.ChangeInsertData(null, null, null, null, null, E.target.value, NewData))}
/>
onChange 側で new Date(InputDeadline) に戻す。
Q&A: ?. とは
考えていたこと
NewData?.Deadline の ? が何か分からない。
問: ? はどういう意味?
解答:
| 記法 | 意味 |
|---|---|
obj?.prop |
obj が null/undefined なら undefined を返す(オプショナルチェイニング) |
prop?: Type |
型定義でプロパティ省略可 |
a ?? b |
a が null/undefined なら b を使う |
useState<Types.Data>() のように初期値なしだと undefined になり得るので、?. や初期値設定が必要。
Q&A: <select> は正しく動く?
考えていたこと
状態をプルダウンにした。これで動く?
問: select + value + onChange の書き方は合ってる?
解答:
-
onChangeでChangeInsertDataの第5引数に渡す 順番は正しい - ただし
useStateに初期値がない とvalueが option と一致せず不安定 -
初期
Types.Dataを渡し、Statusを"未着手"など option のいずれかにする のが安全
案件削除
変更ファイル一覧
-
新規:
src/utils/DeleteData.ts -
加筆:
src/components/DataTable.tsx(削除ボタン列)
問: newData から特定オブジェクトを削除する最適な方法は?
解答: filter で不変更新 が React 向き。
export function DeleteData(Data: Types.Data[], Index: number) {
return Data.filter((_, i) => i !== Index);
}
| 方法 | メリット | デメリット |
|---|---|---|
filter + id
|
安全・拡張しやすい |
id 追加が必要 |
filter + index
|
書きやすい | 並び替えに弱い |
splice(破壊的) |
一見シンプル | React で再描画漏れの原因 |
注意: 将来は CaseName ではなく id で一意管理 した方がよい。
案件更新 UI の設計
考えていたこと
更新 UI をどうするか。2案で迷った。
案1 vs 案2
| 案 | 内容 | メリット | デメリット |
|---|---|---|---|
| 案1 | 行横の「変更」→ その行だけ input に切替 | 対象が明確、誤更新が少ない | 行 UI が複雑 |
| 案2 | チェック + 登録フォームに値を入れて更新 | フォーム再利用しやすい | 編集中の行が分かりにくい |
採用: まず 案1(行内編集)。DataTable に操作列がある構成と相性がよい。
案1: 行内編集の実装イメージ
変更ファイル一覧
-
加筆:
src/App.tsx(draftData,StartChange,SaveEdit,CancelEdit) -
加筆:
src/types/Types.ts(DataTableProps拡張) -
加筆:
src/components/DataTable.tsx(表示/編集の分岐) -
新規(任意):
src/utils/UpdateData.ts
なぜ App が state を持つのか
問: DataTable ではなく App で draftData を管理する理由は?
解答:
-
Search/InsertData/DataTableが 同じデータを共有 - 子に state を持たせると 二重管理 で検索結果と編集結果がズレる
- 親 = 正本、子 = 表示 + イベント通知 が基本
App → DataTable … draftData を props で渡す(親→子)
DataTable → App … onChangeDraft / onSaveEdit を呼ぶ(子→親)
UI 切り替え
Item.ChangeFlag が true の行だけ input を表示。
{Item.ChangeFlag ? (
<>
<td><input value={DraftData?.CaseName ?? Item.CaseName} ... /></td>
...
<button onClick={onSaveEdit}>保存</button>
<button onClick={onCancelEdit}>キャンセル</button>
</>
) : (
<>
<td>{Item.CaseName}</td>
...
<button onClick={() => onStartChange(Item, Index)}>変更</button>
</>
)}
<>...</>(Fragment)とは
問: Fragment は何のため?
解答:
- 三項演算子の 片側に
<td>を複数並べる と JSX エラー -
<>...</>で 複数要素を1グループ にする(DOM に余計なタグは出ない) -
<tr>直下に<div>は HTML として不正なので、テーブル内では Fragment が向く
つまずき: true 側だけ Fragment を付け、false 側を付け忘れると < が比較演算子 と解釈され「算術演算の左辺…」エラーになる。両方 付ける。
setDraftData((prev) => ...)
問: { ...prev, [key]: value } の動きが分からない。
解答:
-
prev… 更新前のdraftData -
{ ...prev, [key]: value }… コピーを作り、1項目だけ 上書き(不変更新) -
[key]… フィールド名を動的に指定("CaseName"など)
注意: prev が undefined のとき { ...prev, [key]: value } だけでは足りない。「変更」押下時に setDraftData({ ...Item }) で初期化 する。
つまずき集(デバッグ編)
キャンセルが効かない
原因
-
CancelEditがAllDataだけ更新し、表示中のDataを更新していなかった -
filterのコールバックにreturnがなく、ChangeFlagが false に戻らない
対策: Data / AllData 両方 を更新。map + スプレッド推奨。
変更後、input に入力しても変わらない
原因
- 「変更」押下時に
draftData未設定 -
ChangeDraftがprev ? ... : prevで undefined 時は更新しない -
value={DraftData?.x ?? Item.x}のフォールバックで 常に元の値 が表示される
対策: StartChange で setDraftData({ ...Item }) を必ず実行。
登録ボタンを押しても追加されない
原因
-
useState<Types.Data>()初期値なし →NewDataがundefined - 初回
onChangeでChangeInsertData(..., undefined)がNewData.xxx参照で例外 →setNewData未到達 -
onAddDataのif (newAddData == null) returnがundefinedも弾く
対策:
const [NewData, setNewData] = useState<Types.Data>({
CaseName: "",
CustomerName: "",
Details: "",
Amount: 0,
Status: "未着手",
Deadline: new Date(),
ChangeFlag: false,
});
An error occurred in the <DataTable> component
原因: React のラッパーメッセージ。Console 直上 の TypeError が本体。
-
Dataにundefinedが入った(未入力登録) -
Item.Deadline.toLocaleDateStringでDeadlineがDateでない
chrome-extension:// エラー
アプリのバグではない。 ブラウザ拡張(フォーム系)のエラー。シークレットモードで切り分け。
JSX の props 位置
<DataTable ... />
onStartChange={StartChange} // ← コンポーネントの外。{StartChange} が子要素扱い
正: すべて /> の 前 に書く。
型定義(現状のイメージ)
export type Data = {
CaseName: string;
CustomerName: string;
Details: string;
Amount: number;
Status: string;
Deadline: Date;
ChangeFlag: boolean;
};
export type DataTableProps = {
Data: Data[];
DraftData: Data;
SetData: (Next: Data[]) => void;
onChangeDraft: <K extends keyof Data>(key: K, value: Data[K]) => void;
onSaveEdit: () => void;
onCancelEdit: () => void;
onStartChange: (Item: Data, Index: number) => void;
};
学んだことまとめ
| テーマ | ポイント |
|---|---|
| 追加 |
AllData に追加し、Data も更新。初期 state 必須 |
| 削除 |
filter で不変更新。将来は id 管理 |
| 更新 | 案1 = ChangeFlag + draftData。編集開始時にコピー |
| state | 共有データは App に集約。子は props + コールバック |
| フォーム | input は string、Date は表示時だけ変換 |
| Fragment | 三項演算子の 両側 を <>...</> で包む |
| デバッグ |
== null は undefined も弾く。Console 直上のエラーを読む |
動作確認チェックリスト
- 案件を登録すると一覧が1件増える
- 削除ボタンで行が消える
- 「変更」→ 入力 → 「保存」で反映、「キャンセル」で元に戻る
- 検索後も
AllDataの整合性が崩れない - DevTools で
draftData/NewDataが undefined のままになっていない
あとがき
出力プロンプトを変えてみました。
あと、実はこのツール自体は完成しているんですけど、
途中からミスって別チェットに質問しちゃったせいで
まとまってないところもあったり・・・。
あと、自分の学習としては生成AIにQAできるのは面白いけど、
それをだらだらと記事にまとめてもなぁ・・・という気持ちが・・・。
次はもう少し面白いものを書けたらと思います。
参考リンク
副業・ポートフォリオ学習用。お題をもとに React を実装しながら整理したシリーズ第2回(追加・削除・更新)。