React は、あなたが目にするデザインや構築するアプリについての考え方を変える可能性があります。React でユーザーインターフェースを構築する場合、まずそれをコンポーネントと呼ばれるパーツに分解します。次に、各コンポーネントのさまざまな視覚的な状態を記述します。最後に、データがそれらを流れるようにコンポーネントを接続します。このチュートリアルでは、React を使用して検索可能な製品データテーブルを構築する思考プロセスを説明します。

モックアップから始める

JSON API と、デザイナーからのモックアップがすでに存在すると想定してください。

JSON API は、次のようなデータを返します。

[
{ category: "Fruits", price: "$1", stocked: true, name: "Apple" },
{ category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },
{ category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },
{ category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },
{ category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },
{ category: "Vegetables", price: "$1", stocked: true, name: "Peas" }
]

モックアップは次のようになります。

React で UI を実装するには、通常、同じ 5 つのステップに従います。

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

モックアップ内のすべてのコンポーネントとサブコンポーネントの周りにボックスを描き、名前を付けることから始めます。デザイナーと協力している場合は、デザインツールでこれらのコンポーネントに名前を付けている可能性があります。彼らに尋ねてみてください!

あなたのバックグラウンドに応じて、デザインをさまざまな方法でコンポーネントに分割することを考えることができます。

  • プログラミング—新しい関数またはオブジェクトを作成する必要があるかどうかを判断する場合と同じ手法を使用します。そのような手法の 1 つに、単一責任の原則があります。つまり、コンポーネントは理想的には 1 つのことだけを行う必要があります。最終的に肥大化する場合は、より小さなサブコンポーネントに分解する必要があります。
  • CSS—クラスセレクターを作成する場合を検討します。(ただし、コンポーネントは少し粒度が低くなります。)
  • デザイン—デザインのレイヤーをどのように整理するかを検討します。

JSON が適切に構造化されている場合、UI のコンポーネント構造に自然にマッピングされることがよくあります。これは、UI とデータモデルが同じ情報アーキテクチャ、つまり同じ形状を持つことが多いためです。UI をコンポーネントに分割し、各コンポーネントがデータモデルの 1 つの部分と一致するようにします。

この画面には 5 つのコンポーネントがあります。

  1. FilterableProductTable (グレー) にはアプリ全体が含まれています。
  2. SearchBar (青) はユーザー入力を受け取ります。
  3. ProductTable (ラベンダー) は、ユーザー入力に従ってリストを表示およびフィルター処理します。
  4. ProductCategoryRow (緑) は、各カテゴリの見出しを表示します。
  5. ProductRow (黄) は、各製品の行を表示します。

ProductTable (ラベンダー) を見ると、テーブルヘッダー(「Name」と「Price」ラベルを含む)が独自のコンポーネントではないことがわかります。これは好みの問題であり、どちらでもかまいません。この例では、ProductTable のリスト内に表示されるため、ProductTable の一部です。ただし、このヘッダーが複雑になる場合 (たとえば、ソートを追加する場合)、独自の ProductTableHeader コンポーネントに移動できます。

モックアップ内のコンポーネントを識別したので、それらを階層に配置します。モックアップ内の別のコンポーネント内に表示されるコンポーネントは、階層内の子として表示される必要があります。

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

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

コンポーネントの階層ができたら、アプリを実装する時間です。最も簡単な方法は、インタラクティビティをまったく追加せずに、データモデルから UI をレンダリングするバージョンを構築することです... まだ!静的なバージョンを最初に構築し、後でインタラクティビティを追加する方が簡単な場合がよくあります。静的なバージョンを構築するには、多くの入力が必要で、考える必要はありません。一方、インタラクティビティを追加するには、多くの思考が必要で、入力はそれほど多くありません。

データモデルをレンダリングする静的なバージョンのアプリを構築するには、他のコンポーネントを再利用し、コンポーネントを構築し、propsを使用してデータを渡す必要があります。propsは、親から子へデータを渡す方法です。(stateの概念に慣れている場合は、この静的なバージョンを構築する際にstateを一切使用しないでください。stateは、インタラクティビティ、つまり時間とともに変化するデータのみのために予約されています。これはアプリの静的なバージョンであるため、stateは必要ありません。)

階層の上位にあるコンポーネント(FilterableProductTableなど)から構築を開始する「トップダウン」方式、または下位のコンポーネント(ProductRowなど)から作業する「ボトムアップ」方式のどちらかで構築できます。簡単な例では、通常はトップダウン方式の方が簡単で、大規模なプロジェクトではボトムアップ方式の方が簡単です。

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Search..." />
      <label>
        <input type="checkbox" />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

function FilterableProductTable({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

(このコードが難解に見える場合は、まずクイックスタートを読んでください!)

コンポーネントを構築すると、データモデルをレンダリングする再利用可能なコンポーネントのライブラリが作成されます。これは静的なアプリであるため、コンポーネントはJSXのみを返します。階層の最上位にあるコンポーネント(FilterableProductTable)は、データモデルをpropsとして受け取ります。これは、データがトップレベルのコンポーネントからツリーの下位にあるコンポーネントに流れるため、一方向データフローと呼ばれます。

落とし穴

この時点では、stateの値を使用しないでください。それは次のステップのためのものです!

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

UIをインタラクティブにするには、ユーザーが基になるデータモデルを変更できるようにする必要があります。このためにstateを使用します。

stateは、アプリが覚えておく必要がある変化するデータの最小限のセットだと考えてください。stateを構造化するための最も重要な原則は、DRY(Don’t Repeat Yourself:同じことを繰り返さない)を維持することです。アプリケーションに必要なstateの絶対的な最小限の表現を把握し、他のすべてをオンデマンドで計算します。たとえば、買い物リストを作成する場合、stateにアイテムを配列として保存できます。リスト内のアイテムの数も表示したい場合は、アイテムの数を別のstateの値として保存するのではなく、配列の長さを読み取ります。

次に、このサンプルアプリケーション内のすべてのデータ要素について考えてみましょう。

  1. 製品の元のリスト
  2. ユーザーが入力した検索テキスト
  3. チェックボックスの値
  4. フィルタリングされた製品のリスト

これらのうちどれがstateでしょうか?そうでないものを特定してください。

  • 時間の経過とともに変化しないか?もしそうなら、それはstateではありません。
  • propsを介して親から渡されるか?もしそうなら、それはstateではありません。
  • コンポーネント内の既存のstateまたはpropsに基づいて計算できるか?もしそうなら、それは絶対にstateではありません!

残ったものがおそらくstateです。

もう一度、1つずつ確認してみましょう。

  1. 製品の元のリストは、propsとして渡されるため、stateではありません。
  2. 検索テキストは、時間の経過とともに変化し、何からも計算できないため、stateのようです。
  3. チェックボックスの値は、時間の経過とともに変化し、何からも計算できないため、stateのようです。
  4. フィルタリングされた製品のリストは、元の製品リストを取得し、検索テキストとチェックボックスの値に従ってフィルタリングすることで計算できるため、stateではありません。

つまり、検索テキストとチェックボックスの値のみがstateです!よくできました!

詳細解説

props vs state

Reactには2種類の「モデル」データがあります。それはpropsとstateです。この2つは大きく異なります。

propsとstateは異なりますが、連携して機能します。親コンポーネントは、多くの場合、stateに何らかの情報を保持し(変更できるように)、それを子コンポーネントのpropsとして渡します。最初に読んだときに違いがまだ曖昧に感じられるのは大丈夫です。実際に身につけるには、少し練習が必要です!

ステップ4:stateを配置する場所を特定する

アプリの最小限のstateデータを特定したら、このstateを変更する責任を持つコンポーネント、つまりstateを所有するコンポーネントを特定する必要があります。覚えておいてください。Reactは一方向データフローを使用し、コンポーネント階層内で親から子コンポーネントにデータを渡します。どのコンポーネントがどのstateを所有すべきかをすぐに理解できない場合があります。この概念に慣れていない場合は難しいかもしれませんが、次の手順に従うことで解決できます!

アプリケーション内のstateの各要素について

  1. そのstateに基づいて何かをレンダリングするすべてのコンポーネントを特定します。
  2. それらの最も近い共通の親コンポーネント(階層内のすべての上位にあるコンポーネント)を見つけます。
  3. stateを配置する場所を決定します。
    1. 多くの場合、stateを共通の親に直接配置できます。
    2. また、共通の親の上位にあるコンポーネントにstateを配置することもできます。
    3. stateを所有するのに適切なコンポーネントが見つからない場合は、stateを保持するためだけの新しいコンポーネントを作成し、共通の親コンポーネントよりも上の階層のどこかに追加します。

前のステップでは、このアプリケーションに2つのstate要素(検索入力テキストとチェックボックスの値)があることがわかりました。この例では、それらは常に一緒に表示されるため、同じ場所に配置するのが理にかなっています。

次に、それらに対する戦略を実行してみましょう。

  1. stateを使用するコンポーネントを特定します。
    • ProductTableは、そのstate(検索テキストとチェックボックスの値)に基づいて製品リストをフィルタリングする必要があります。
    • SearchBar は、その状態(検索テキストとチェックボックスの値)を表示する必要があります。
  2. 共通の親を見つける: 両方のコンポーネントが共有する最初の親コンポーネントは、FilterableProductTable です。
  3. 状態を保持する場所を決定する: フィルターテキストとチェック状態の値は、FilterableProductTable に保持します。

したがって、状態の値は FilterableProductTable に存在することになります。

useState()フックを使って、コンポーネントに状態を追加します。フックは、Reactに「フックイン」できる特別な関数です。FilterableProductTable の先頭に2つの状態変数を追加し、初期状態を指定します。

function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);

次に、filterTextinStockOnly を props として ProductTableSearchBar に渡します。

<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>

アプリケーションがどのように動作するかを確認できます。サンドボックスコードの useState('') から useState('fruit') へ、filterText の初期値を編集してください。検索入力テキストとテーブルの両方が更新されるのが確認できます。

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} />
      <ProductTable 
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -1
    ) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar({ filterText, inStockOnly }) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} 
        placeholder="Search..."/>
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

フォームの編集はまだ機能していないことに注意してください。上記のサンドボックスには、その理由を説明するコンソールエラーがあります。

コンソール
`onChange` ハンドラーなしで、フォームフィールドに `value` prop を渡しました。これにより、読み取り専用のフィールドがレンダリングされます。

上記のサンドボックスでは、ProductTableSearchBar は、filterTextinStockOnly の props を読み取り、テーブル、入力、チェックボックスをレンダリングします。たとえば、SearchBar が入力値を設定する方法は次のとおりです。

function SearchBar({ filterText, inStockOnly }) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."/>

ただし、まだ入力などのユーザーアクションに対応するコードを追加していません。これが最後のステップになります。

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

現在、アプリは props と状態が階層を下って流れることで正しくレンダリングされています。しかし、ユーザー入力に応じて状態を変更するには、データが別の方向に流れるのをサポートする必要があります。階層の深いところにあるフォームコンポーネントは、FilterableProductTable の状態を更新する必要があります。

React はこのデータフローを明示的にしますが、双方向データバインディングよりも少し多くの記述が必要になります。上記の例で入力したりチェックボックスをオンにしたりしようとすると、React が入力を無視することがわかります。これは意図的なものです。<input value={filterText} /> を記述することで、inputvalue prop は常に FilterableProductTable から渡された filterText の状態と等しくなるように設定しました。filterText の状態は設定されないため、入力が変更されることはありません。

ユーザーがフォーム入力を変更するたびに、状態がそれらの変更を反映するように更新するようにします。状態は FilterableProductTable によって所有されているため、setFilterTextsetInStockOnly を呼び出すことができるのはそれだけです。SearchBarFilterableProductTable の状態を更新できるようにするには、これらの関数を SearchBar に渡す必要があります。

function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);

return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />

SearchBar の内部で、onChange イベントハンドラーを追加し、それらから親の状態を設定します。

function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange
}) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)}

これで、アプリケーションは完全に機能します。

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} 
        onFilterTextChange={setFilterText} 
        onInStockOnlyChange={setInStockOnly} />
      <ProductTable 
        products={products} 
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -1
    ) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} placeholder="Search..." 
        onChange={(e) => onFilterTextChange(e.target.value)} />
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} 
          onChange={(e) => onInStockOnlyChange(e.target.checked)} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

イベントの処理と状態の更新について詳しくは、「インタラクティビティの追加」セクションで学ぶことができます。

次のステップ

これは、React でコンポーネントとアプリケーションを構築する方法についての非常に簡単な入門でした。今すぐReact プロジェクトを開始するか、このチュートリアルで使用されているすべての構文についてさらに深く掘り下げてください。