1
1

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学習 案件管理ツールの作成

1
Posted at

初めに

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

備忘録として記事を残していきます。

今回は①案件一覧と⑤検索機能に向けて、検索部・一覧部のコンポーネント分割props親子間のデータの流れまでを実装しながら整理したメモです。

また、cursorで記事を書けるかの実験も兼ねています。

スタック: React 19 / TypeScript / Vite
読者想定: React 初級〜中級

この記事でやること

項目 内容
画面構成 検索部(Search)+ データ表示部(DataTable
Types.tsData / Search / props 用の型
ロジック utils/DataFilter.ts で絞り込み
データの流れ App → Search(ボタン)→ App → DataTable

プロジェクトの全体像

最終的な機能(目標)

  1. 案件一覧(案件名・顧客名・ステータス・金額・納期・担当者)← 今回
  2. 案件追加
  3. 編集
  4. 削除
  5. 検索 ← 今回

ディレクトリ構成(現時点)

構成
src/
  App.tsx
  types/Types.ts
  components/
    Search.tsx
    DataTable.tsx
  utils/
    DataFilter.ts
    Index.ts

コンポーネントは2つに分ける

考えていたこと:
Reactは初心者であり、以前に簡単なツールを作った程度で正直何もわかっていない状態です。
前回のツール作成時にコンポーネントという単語を知り、今回から意識することに。
まだ、コンポーネントがどんなものなのか。どれくらいの数に分けるのが一般的なのかよくわかっていないのでAIと相談しながら進めていきます。

結論: 検索 UI と一覧表示は 別コンポーネント に分けてよい。
state(案件一覧・検索条件・表示用データ)は 親の App に置く。

構成イメージ
App(state を持つ)
 ├── Search(入力 + 検索ボタン)
 └── DataTable(表で表示)

理由

  • 検索欄だけ・表だけを差し替えやすい
  • 編集・削除を足すときにフォームコンポーネントを追加しやすい
  • 記事・ポートフォリオとして説明しやすい

今回作成するものは画面の要素分けがわかりやすかったため、想像通りの構成でした。

コンポーネントを分けたら「関数定義」まで書く?

考えていたこと:
コンポーネントについては「あー、なんかHTMLをブロックごとに分けるんだな~」くらいの理解で次の作業に。
ぶっちゃけ、分けてみたはいいもののいまいち何を書くかわかっていませんでした。
App.tsxみたいにHTMLをreturnするのは想像できていたのですが、その中で呼び出す想定のメソッド定義などはどうするんだろというのが次の疑問。

システム構成はしっかり考えていきたいところ。

実は新しいツールを作る前に以前のツールを拡張していく案も実はありましたが、
1ファイルにいろいろ詰め込まれすぎて読むの嫌になったし、コンポーネントやメソッド定義を分けるには遅すぎて萎えました。

これからReactを触る方は同じ過ちを犯さないようにしましょう。
コンポーネントも分けず、すべてのメソッドをAPP.tsxに記載するとメンテナンス性が終わります、まじで

AIの解答
はい。 ファイルを分ける = その部品用の関数コンポーネントを1ファイルに書く のが一般的です。

DataTable.tsx
import type { DataTableProps } from "../types/Types";

export function DataTable({ Data }: DataTableProps) {
  return (
    <table>
      <thead>
        <tr>
          <th>案件名</th>
          <th>顧客名</th>
        </tr>
      </thead>
      <tbody>
        {Data.map((Item) => (
          <tr key={Item.CaseName}>
            <td>{Item.CaseName}</td>
            <td>{Item.CustomerName}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

filter のような JSX を返さない処理utils/DataFilter.ts に分離します。


(ん~、聞き方が悪かったかなと思いつつ自分で調べてメソッド定義用のファイルを別に切っていくことにします。)

props 入門

考えていたこと:
コンポーネント分け、メソッド定義が終われば値の受け渡しです。
イメージしていた流れは
APP→コンポーネント→メソッド→コンポーネント→APP

コンポーネント→メソッド:引数での受け渡し
メソッド→コンポーネント:returnでの受け渡し

ここまではわかるのですが、

APP→コンポーネント
コンポーネント→APP

ここがよくわからないので解説してもらいました。

props とは

親から子へ渡す引数 です。子は基本的に 読むだけ(親の state を子で直接書き換えない)。

定義(TypeScript)

Types.ts
export type Data = {
  CaseName: string;
  CustomerName: string;
  Details: string;
  Amount: number;
  Status: string;
  Deadline: Date;
};

export type Search = {
  SearchCaseName: string;
  SearchCustomerName: string;
};

export type SearchProps = {
  SearchItems: Search;
  Data: Data[];
  SetSearchItems: (Next: Search) => void;
  SetData: (Next: Data[]) => void;
};

export type DataTableProps = {
  Data: Data[];
};

子:受け取る

Search.tsx
export function Search({
  SearchItems,
  Data,
  SetSearchItems,
  SetData,
}: Types.SearchProps) {
  // ...
}

親:渡す

App.tsx
<Search
  SearchItems={SearchItems}
  Data={AllData}
  SetSearchItems={SetSearchItems}
  SetData={SetData}
/>
<DataTable Data={Data} />
記法 意味
Data={AllData} 変数 AllData を渡す
Data="AllData" 文字列 "AllData" を渡す(意図と違いやすい)

prop 名と変数名は一致しなくてよい。 例: Data={AllData} のように、意味が分かる名前で渡してよい。


皆さんはこれでわかりましたかね?
自分は理解できなさ過ぎてキレてました。
結局自分で調べたり、AIに追加で聞いてみたりした結果としては

  1. コンポーネントにはオブジェクトを1つ引数で渡せる
  2. 渡したいものをすべて詰め込める型を定義して、詰め込む
  3. コンポーネント→APPはAPPで定義したuseStateのset関数を
    オブジェクトに詰めることで実現する

という理解になりました。
もし間違えていたら教えてください。

ここまでが特有のもので苦戦したところになります。

これ以降は単純なミスだったり、ど忘れしてた文法について生成AIが出力したものをそのまま記載します。

つまずき1: search と Search(PascalCase)

結論: JSX では 小文字始まりは HTML タグ大文字始まりは自作コンポーネント です。

書き方 結果
<search /> HTML 扱い → Search.tsx の関数は呼ばれない
<Search /> import したコンポーネントが動く
NG例.tsx
import { search } from "./components/Search";
<search SearchItems={SearchItems} />
OK例.tsx
import { Search } from "./components/Search";
<Search SearchItems={SearchItems} />

関数定義を PascalCase にしても、JSX が小文字のままだと直らない。 定義・import・JSX の3つを揃えます。

つまずき2: モックデータの作り方

NG例.tsx
const TestData: Types.Data = Types.Data;
TestData.CaseName = "test";

Types.Data であり 値ではない ので、右辺に書けません。

OK例.tsx
const TestData: Types.Data = {
  CaseName: "test",
  CustomerName: "テスト企業",
  Details: "",
  Amount: 0,
  Status: "進行中",
  Deadline: new Date(),
};
const [AllData, SetAllData] = useState<Types.Data[]>([TestData]);
const [Data, SetData] = useState<Types.Data[]>([TestData]);
  • 左の : Types.Data → 型注釈(コンパイル時)
  • 右の { ... } → 実行時の値

データの流れ(検索ボタン方式)

想定フロー

フロー
App
 → Search(検索ボタン押下で DataFilter)
 → SetData で App の表示用 state を更新
 → DataTable が更新された Data を表示

App.tsx

App.tsx
import { useState } from 'react'
import { DataTable } from "./components/DataTable";
import { Search } from "./components/Search";
import type * as Types from "./types/Types";
import './App.css'

function App() {
  const TestData: Types.Data = {
    CaseName: "test",
    CustomerName: "テスト企業",
    Details: "",
    Amount: 0,
    Status: "進行中",
    Deadline: new Date(),
  };
  const [AllData, SetAllData] = useState<Types.Data[]>([TestData]);
  const [Data, SetData] = useState<Types.Data[]>([TestData]);
  const [SearchItems, SetSearchItems] = useState<Types.Search>({
    SearchCaseName: "",
    SearchCustomerName: "",
  });

  return (
    <>
      <Search
        SearchItems={SearchItems}
        Data={AllData}
        SetSearchItems={SetSearchItems}
        SetData={SetData}
      />
      <DataTable Data={Data} />
    </>
  )
}

export default App
  • AllData … フィルタの 元になる全件
  • Data表に表示するデータ

Search.tsx

Search.tsx
import * as Utils from "../utils/Index";
import type * as Types from "../types/Types";

export function Search({ SearchItems, Data, SetSearchItems, SetData }: Types.SearchProps) {

    const ChangeSearch = (SearchCaseName: string | null, SearchCustomerName: string | null) => {
        const NewSearch = { ...SearchItems };
        if (SearchCaseName != null) {
            NewSearch.SearchCaseName = SearchCaseName;
        }
        if (SearchCustomerName != null) {
            NewSearch.SearchCustomerName = SearchCustomerName;
        }
        SetSearchItems(NewSearch);
    }

    return (
        <div>
            <table>
                <tr>
                    <th>案件名</th>
                    <th>顧客名</th>
                </tr>
                <tr>
                    <td><input type="text" value={SearchItems.SearchCaseName} onChange={(E) => ChangeSearch(E.target.value, null)} /></td>
                    <td><input type="text" value={SearchItems.SearchCustomerName} onChange={(E) => ChangeSearch(null, E.target.value)} /></td>
                </tr>
            </table>
            <button type="button" onClick={() => SetData(Utils.DataFilter(Data, SearchItems))}>Search</button>
        </div>
    );
}

{ ...SearchItems }新しいオブジェクト を作ってから SetSearchItems すると、再レンダーが安定します。

utils/DataFilter.ts(現状)

DataFilter.ts
import type * as Types from "../types/Types";

export function DataFilter(Data: Types.Data[], SearchItems: Types.Search) {

    if(SearchItems.SearchCaseName == "" && SearchItems.SearchCustomerName == ""){
        return Data
    }else{
        return Data.filter((Item) =>
            Item.CaseName ==  SearchItems.SearchCaseName ||
            Item.CustomerName == SearchItems.SearchCustomerName);
    }
}

つまずき3: 検索が効かない・おかしい

原因A: includes("") と OR(初期案でハマった例)

NG例.ts
Item.CaseName.includes(SearchItems.SearchCaseName) ||
Item.CustomerName.includes(SearchItems.SearchCustomerName);

空文字は "何か".includes("") === true なので、片方の入力が空だと OR で全件ヒット しやすい。

改善案(部分一致 + 空欄無視 + AND):

DataFilter改善案.ts
export function DataFilter(Data: Types.Data[], SearchItems: Types.Search) {
  return Data.filter((Item) => {
    const MatchCase =
      SearchItems.SearchCaseName === "" ||
      Item.CaseName.includes(SearchItems.SearchCaseName);
    const MatchCustomer =
      SearchItems.SearchCustomerName === "" ||
      Item.CustomerName.includes(SearchItems.SearchCustomerName);
    return MatchCase && MatchCustomer;
  });
}

原因B: 絞り結果で元データを上書き

SetData(DataFilter(Data, ...))Dataすでに絞られた配列 だと、2回目以降や全件復帰ができない。

対策: AllData(全件)と Data(表示用)を分け、フィルタは常に AllData にかける。

App.tsx
<Search Data={AllData} SetData={SetData} />
<DataTable Data={Data} />

学んだことまとめ

テーマ ポイント
分割 Search / DataTable、state は App
props 型定義 → 子で受け取り → 親で prop={値}
JSX コンポーネントは <Search />(PascalCase)
型と値 Types.Data は型、データは { ... }
検索 SetData で親に戻す流れは正しい。filter 条件と全件保持が肝

完成した機能

  • 検索機能
  • 案件データ画面表示

次にやること

  • 案件追加機能
  • 案件削除機能

参考リンク


副業・ポートフォリオ学習用。お題をもとに React を実装しながら整理したシリーズ第1回。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?