2025年1月9日

Reactの流儀を実践してみた

  • 以下のモック画像があったとします。

mock-for-ui


  • このUIを実装するために、Reactの流儀の5つのステップを行います。

ステップ 1: UI をコンポーネントの階層に分割する

  • 問い
    • 全てのコンポーネントを四角で囲って、それぞれに名前をつけてください。
  • 実践
    • FilterableBlogCards
      • 責務: ブログ一覧全体のコンテナ
    • BlogSearchBar
      • 責務: ブログの情報を記載
      • より詳細を作るのであれば:(CardImage, CardContent, CardLink)

divide-components


  • Single Responsibility
    • proncipleを元にコンポーネントに分割を行う事でコンポーネントは1つの責務に集中できてコード量が増えません
    • また、枠の内側にあるコンポーネント同士は同じ階層に置くことができるでしょう。

ステップ2: Reactで静的なバージョンを作成する

  • 問い
    • インタラクティブな要素はまだ咥えずにデータからUIをレンダーするようにしましょう。
    • 各子コンポーネントにはprops経由でデータを渡しましょう。
    • コンポーネントは高階層でも低階層でもどちらでもいいです。
    • 一般的には単純な場合は高階層から、複雑な場合は低階層からコンポーネントを作成し始めます。
  • 実践
    • データがトップレベルのコンポーネントからツリーの下にあるコンポーネントに流れるため単方向データフローと呼ばれている。
// フィルタリング機能付きのブログ一覧
function FilterableBlogCards({blogs}) {
  return (
    <div>
      <BlogSearchBar searchName={""} />

      {blogs.map((blog) => {
        return <BlogCard blog={blog} key={blog.id} />
      })}
    </div>
  )
}

function BlogSearchBar({searchName}) {
  return (
    <div className="flex justify-center">
      <input
        className="長いので省略..."
        value={searchName}
        placeholder="キーワードを入力..."
      />
    </div>
  )
}

function BlogCard({blog}) {
  return (
    <Card
      className="m-2 flex flex-col justify-center sm:w-1/3 md:w-1/5"
      key={blog.year + blog.month + blog.date + blog.title}
    >
      <CardLink to={""}>
        <CardImage type={cardType} />

        <CardContent title={blog.title} year={blog.year} month={blog.month} date={blog.date} />
      </CardLink>
    </Card>
  )
}

// 以下のコンポーネントは省略
function Card() {
  return ...
}
function CardLink() {
  return ...
}
function CardImage() {
  return ...
}
function CardContent() {
  return ...
}

ステップ3: UI の状態を最小限かつ完全に表現する方法を見つける

  • 問い
    • 必要な状態を考えてください。
      • まずはDRYの原則を意識して、stateの構造を考えましょう。
      • 時間が経つと変わるものや、親からpropsで渡されないものなどで最小限のデータをstateでアプリに記憶させます。
  • 実践
    • searchNameはユーザーの入力によって変わるものなのでstateです。
    • blogsはsearchNameによってフィルタリングされるので、stateです。
    • blogsは今回、stateと判断しましたが、プロダクトや開発チームによってはuseMemo,useContextなどのReact Hooksを使う場合やZustandやJotaiなどの状態管理ライブラリで状態を管理するパターンも考えられると思います。また、React Hooksや状態管理ライブラリを使わずにシンプルにjavascriptのfilterメソッドやmapメソッドを用いてblogsを計算(フィルタリング)する場合もありえます。

decided-state

ステップ 4: state を保持すべき場所を特定する

  • 問い
    • 1.そのstateに基づいてレンダーするコンポーネントを特定
    • 2.階層内で共通の親コンポーネントを見つける
    • 3.stateがどこにあるべきかを決定する
    • 多くの場合はstateを共通の親に置く
    • stateを所有する適切なコンポーネントが見つからない場合
    • stateを保持するためだけの新しいコンポーネントを探して、共通の親コンポーネントの上のどこかに追加する
  • 実践
    • 1.FilterableBlogCardsコンポーネントでは検索テキストに基づいて、ブログデータ(blogs)をフィルタリングする必要がある。またSearchBarコンポーネントは検索テキストを表示する必要がある。
    • 2.両方のコンポーネントに共通する最初の親コンポーネントはFilterableBlogCardsコンポーネント
    • 3.フィルタリングしたブログの状態と検索テキストはFilterableBlogCardsに保持することに決定
// フィルタリング機能付きのブログ一覧
function FilterableBlogCards({blogs}) {
  const [searchName, setSearchName] = useState('')
  const [filteredBlogs, setFilteredBlogs] = useState(blogs)

  // filteredBlogsとsearchNameを表示または子コンポーネントに渡す
  return ...
}

ステップ 5: 逆方向のデータフローを追加する

  • 問い
    • ユーザーの検索テキスト入力によってstateが変更して反映されるようにしてください。
    • 現段階ではprops と stateが階層構造の下方向に向かって流れているのみの状態です。
    • 現段階ではprops と stateが階層構造の下方向に向かって流れているのみの状態です。

現在のデータフロー

one-way-data-flow

  • 実践
    • イベントハンドラを渡した後のデータフロー
    • 双方向データバインディングよりタイプは増えるが、Reactではデータフローを明示的に記述することが求められる。
// フィルタリング機能付きのブログ一覧
function FilterableBlogCards({blogs}) {
  const [searchName, setSearchName] = useState('')

  // イベントハンドラを定義
  const handleHangeSearchName = (e: ChangeEvent<HTMLInputElement>) => {
    setSearchName(e.target.value)
  }

  // ブログのフィルタリング処理
  ...

  return (
    <div>
      {/* 定義したイベントハンドラを渡す */}
      <BlogSearchBar searchName={searchName} onChange={handleHangeSearchName} />

    ...
  )
}

reserve-direction


いかがだったでしょうか? Reactの流儀の5つのステップを行ったことでReactにおける開発のメンタルモデルが少しでも形成されていたら嬉しいです。 最終的なコードを載せます。 一部、ステップには載せなかった記述もあります。 必要に応じて確認してください。

// フィルタリング機能付きのブログ一覧
function FilterableBlogCards({blogs}) {
  const [searchName, setSearchName] = useState('')
  const [filteredBlogs, setFilteredBlogs] = useState(blogs)

  const handleHangeSearchName = (e: ChangeEvent<HTMLInputElement>) => {
    setSearchName(e.target.value)
    filterBlogs(e.target.value)
  }

  // 今回紹介していないコードです。
  const filterBlogs = (searchNameValue: string) => {
    const results = BlogTitles.filter((blog) => blog.title.includes(searchNameValue))
    setFilteredBlogs(results)
  }

  return (
    <div>
      <BlogSearchBar searchName={searchName} onChange={handleHangeSearchName} />

      {filteredBlogs.map((blog) => {
        return <BlogCard blog={blog} key={blog.id} />
      })}
    </div>
  )
}

function BlogSearchBar({searchName,onChange}) {
  return (
    <div className="flex justify-center">
      <input
        className="長いので省略..."
        value={searchName}
        placeholder="キーワードを入力..."
        onChange={onChange}
      />
    </div>
  )
}

function BlogCard({blog}) {
  return (
    <Card
      className="m-2 flex flex-col justify-center sm:w-1/3 md:w-1/5"
      key={blog.year + blog.month + blog.date + blog.title}
    >
      <CardLink to={""}>
        <CardImage type={cardType} />

        <CardContent title={blog.title} year={blog.year} month={blog.month} date={blog.date} />
      </CardLink>
    </Card>
  )
}

// 以下のコンポーネントは省略
function Card() {
  return ...
}
function CardLink() {
  return ...
}
function CardImage() {
  return ...
}
function CardContent() {
  return ...
}

参照

Reactの流儀