0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React学習 案件管理ツールの作成②

0
Posted at

副業に向けて React を学び、ポートフォリオ用に 案件管理ツール(CaseManagementTool) を Vite + TypeScript で開発しています。

前回は検索機能まで実装しました。今回は⑥ データコンポーネントと案件の追加・削除・更新 に向けて、UI 改善・コンポーネント分割・state 管理・つまずきを実装しながら整理したメモです。

スタック: React 19 / TypeScript / Vite
読者想定: React 初級〜中級
前回: 検索(Search / DataFilter / AllDataData の分離)

この記事でやること

項目 内容
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.tsInsertProps
  • 加筆: src/App.tsxonAddData

登録の流れ

フロー
InsertData(入力)
 → onAddData(App)
 → AllData / Data に追加
App.tsx(加筆)
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 DateYYYY-MM-DD(date input 用)

学んだこと: DataFilter と同様、表示はコンポーネント・ロジックは utils に分けると後から楽。


Q&A: フォームと Date

考えていたこと
期限 input の valueDate をそのまま渡したら型エラーになった。

問: value={NewData.Deadline} で「型 'Date' を型 'string | number | readonly string[]' に割り当てることはできません」と出る。どう直す?

解答:

  • <input>value文字列(または number)
  • Types.Data.DeadlineDate
  • フォームでは文字列、ドメイン型では Date と分ける
InsertData.tsx(加筆)
<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 の書き方は合ってる?

解答:

  • onChangeChangeInsertData の第5引数に渡す 順番は正しい
  • ただし useState に初期値がないvalue が option と一致せず不安定
  • 初期 Types.Data を渡し、Status"未着手" など option のいずれかにする のが安全

案件削除

変更ファイル一覧

  • 新規: src/utils/DeleteData.ts
  • 加筆: src/components/DataTable.tsx(削除ボタン列)

問: newData から特定オブジェクトを削除する最適な方法は?

解答: filter で不変更新 が React 向き。

DeleteData.ts(新規)
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.tsxdraftData, StartChange, SaveEdit, CancelEdit
  • 加筆: src/types/Types.tsDataTableProps 拡張)
  • 加筆: src/components/DataTable.tsx(表示/編集の分岐)
  • 新規(任意): src/utils/UpdateData.ts

なぜ App が state を持つのか

問: DataTable ではなく AppdraftData を管理する理由は?

解答:

  • Search / InsertData / DataTable同じデータを共有
  • 子に state を持たせると 二重管理 で検索結果と編集結果がズレる
  • 親 = 正本、子 = 表示 + イベント通知 が基本
データの流れ
App → DataTable   … draftData を props で渡す(親→子)
DataTable → App   … onChangeDraft / onSaveEdit を呼ぶ(子→親)

UI 切り替え

Item.ChangeFlagtrue の行だけ input を表示。

DataTable.tsx(加筆)
{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 } の動きが分からない。

解答:

  1. prev … 更新前の draftData
  2. { ...prev, [key]: value } … コピーを作り、1項目だけ 上書き(不変更新)
  3. [key] … フィールド名を動的に指定("CaseName" など)

注意: prevundefined のとき { ...prev, [key]: value } だけでは足りない。「変更」押下時に setDraftData({ ...Item }) で初期化 する。


つまずき集(デバッグ編)

キャンセルが効かない

原因

  1. CancelEditAllData だけ更新し、表示中の Data を更新していなかった
  2. filter のコールバックに return がなく、ChangeFlag が false に戻らない

対策: Data / AllData 両方 を更新。map + スプレッド推奨。

変更後、input に入力しても変わらない

原因

  • 「変更」押下時に draftData 未設定
  • ChangeDraftprev ? ... : prevundefined 時は更新しない
  • value={DraftData?.x ?? Item.x} のフォールバックで 常に元の値 が表示される

対策: StartChangesetDraftData({ ...Item }) を必ず実行。

登録ボタンを押しても追加されない

原因

  • useState<Types.Data>() 初期値なし → NewDataundefined
  • 初回 onChangeChangeInsertData(..., undefined)NewData.xxx 参照で例外setNewData 未到達
  • onAddDataif (newAddData == null) returnundefined も弾く

対策:

InsertData.tsx(加筆)
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 が本体。

  • Dataundefined が入った(未入力登録)
  • Item.Deadline.toLocaleDateStringDeadlineDate でない

chrome-extension:// エラー

アプリのバグではない。 ブラウザ拡張(フォーム系)のエラー。シークレットモードで切り分け。

JSX の props 位置

NG例.tsx
<DataTable ... />
onStartChange={StartChange}  // ← コンポーネントの外。{StartChange} が子要素扱い

正: すべて /> に書く。


型定義(現状のイメージ)

Types.ts
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. 案件を登録すると一覧が1件増える
  2. 削除ボタンで行が消える
  3. 「変更」→ 入力 → 「保存」で反映、「キャンセル」で元に戻る
  4. 検索後も AllData の整合性が崩れない
  5. DevTools で draftData / NewData が undefined のままになっていない

あとがき

出力プロンプトを変えてみました。
あと、実はこのツール自体は完成しているんですけど、
途中からミスって別チェットに質問しちゃったせいで
まとまってないところもあったり・・・。
あと、自分の学習としては生成AIにQAできるのは面白いけど、
それをだらだらと記事にまとめてもなぁ・・・という気持ちが・・・。

次はもう少し面白いものを書けたらと思います。

参考リンク


副業・ポートフォリオ学習用。お題をもとに React を実装しながら整理したシリーズ第2回(追加・削除・更新)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?