Reactによるコンポーネントパターンまとめ

# Reactによるコンポーネントパターンまとめ

コンポーネントのパターンとして、Render-props, HOCs, Compound components, Container componentsの4種を紹介します。

またそれぞれのパターンに対してサンプルコードとCodeSandboxへのリンクを貼っています。

## Render props

主に`ReactNode`を返す関数をプロパティとして受け取り、それを描画するパターンです。

https://reactjs.org/docs/render-props.html

### サンプル

`render`プロパティで受け取ったものを表示するサンプルです。

```tsx
function RenderPropsSample(props: { render: () => ReactNode }) {
  return <>{props.render()}</>;
}
```

https://codesandbox.io/s/strange-liskov-p6tel

### 実用的な例

複数のrenderプロパティを持たせて、Union型を表示するようなコンポーネントを作ります。

例えば、下のようなPromiseのラッパーを用意します。

```tsx
export type PromiseResult<T> =
  | { type: "rejected"; value: Error }
  | { type: "pending" }
  | { type: "fulfilled"; value: T };
```

これを表示するコンポーネントを作ろうと思ったら、`promiseResult`とその状態ごとのハンドラを受け取るコンポーネントを用意します。
こうすることで、`promiseResult`のパターンマッチのみを一つのコンポーネントに切り出すことができます。

```tsx
interface PromiseRendererProps<T> {
  promiseResult: PromiseResult<T>;
  onPending?: () => ReactNode;
  onRejected?: (error: Error) => ReactNode;
  onFulfilled?: (value: T) => ReactNode;
}

export function PromiseResultRenderer<T>(props: PromiseRendererProps<T>): ReactElement {
  const { promiseResult, onPending, onRejected, onFulfilled } = props;
  switch (promiseResult.type) {
    case "pending":
      return <>{onPending()}</>;
    case "rejected":
      return <>{onRejected ? onRejected(promiseResult.value) : undefined}</>;
    case "fulfilled":
      return <>{onFulfilled ? onFulfilled(promiseResult.value) : undefined}</>;
  }
}
```

下のサンプルでは、10秒のロード時間がかかるasync関数を実行し、Pending中は「Loading...」と表示してFulfilledとなったら結果を表示しています。

https://codesandbox.io/s/practical-hill-wooov?file=/src/App.tsx

尤も、このようなPromiseを表示するテクニックは今後`Suspense`に置き換えられていきそうです。

https://reactjs.org/docs/concurrent-mode-suspense.html

## Higher-order components

コンポーネントを作る関数です。

propsを加工して渡したり、固定値を埋めて渡す場合にはHOCsが便利です。

https://reactjs.org/docs/higher-order-components.html

### サンプル

引数で受け取ったコンポーネントをそのまま返す高階関数です。

```tsx
function higherOrderComponent<P extends {}>(WrappedComponent: ComponentType<P>) {
    return (props: P) => <WrappedComponent {...props}/>
}
```

### 実用的な例

`disabled`プロパティのデフォルト値を`false`ではなく`true`にする高階コンポーネントを定義しています。

`withDisabledByDefault`に渡すコンポーネントは `disabled` プロパティを持っていなければなりません。

```tsx
interface RequiredProps {
  disabled?: boolean;
}

function withDisabledByDefault<P extends RequiredProps>(
  WrappedComponent: React.ComponentType<P>
) {
  return (props: P) => {
    return <WrappedComponent disabled={true} {...props} />;
  };
}
```

## Compound components

暗黙のうちに状態を共有する、親子のコンポーネントです。例えば`ul`タグと`li`タグのようなイメージです。

状態は親が持ち、contextを使って共有します。

### サンプル

`ol`と`li`要素をラップし、最後にクリックされた子コンポーネントの`value`を親コンポーネントが保持するサンプルです。

`state`をまるごとcontextに渡して子コンポーネントに共有しています。
このcontextは親子の間でのみ共有し、外部には公開しないようにしましょう。

```tsx
interface SelectedContextValue {
  selected: number | null;
  setSelected: (selected: number | null) => void;
}

const defaultSelectedContext: SelectedContextValue = {
  selected: null,
  setSelected: () => {}
};

const SelectedContext = createContext<SelectedContextValue>(
  defaultSelectedContext
);

export function SelectableList(props: PropsWithChildren<{}>) {
  const [selected, setSelected] = useState<number | null>(null);
  return (
    <SelectedContext.Provider value={{ selected, setSelected }}>
      <h2>You are selecting {selected ?? "nothing"} now.</h2>
      <ol>{props.children}</ol>
    </SelectedContext.Provider>
  );
}

function useSelectedContext() {
  const context = useContext(SelectedContext);
  return context ? context : defaultSelectedContext;
}

export function SelectableListItem(props: PropsWithChildren<{ value: number }>) {
  const { selected, setSelected } = useSelectedContext();
  const handleClick = useCallback(() => {
    setSelected(props.value);
  }, [props.value, setSelected]);
  const additionalMessage = props.value === selected ? "I'm selected! " : "";
  return (
    <li onClick={handleClick}>
      {additionalMessage}
      {props.children}
    </li>
  );
}
```

https://codesandbox.io/s/sweet-water-hiq37?file=/src/App.tsx

## Container components

見た目には一切影響を与えない、副作用のみを扱うコンポーネントです。

スタイルの設定やPureなロジックを持つPresentationalコンポーネントに対して、例えば`fetch`や`useSelector`で得た値を渡したり、イベントハンドラに`fetch`や`useDispatch`などを使った関数を渡したりします。

### サンプル

ある項目のリストを表示するPresentationalコンポーネントと、サーバーから値を取ってきてそれに渡すContainerコンポーネントのサンプルです。本質的ではないのでサーバーとの通信は行っていませんが、この状態でもモックとしての価値があります。

```tsx
function Presentational(props: { rows: string[] }) {
  return (
    <ul>
      {props.rows.map((row) => {
        return <li>{row}</li>;
      })}
    </ul>
  );
}

function ContainerMock() {
  const sampleRows = ["Hello", "Bonjour", "Guten tag"];
  return <Presentational rows={sampleRows} />;
}
```

https://codesandbox.io/s/cold-sound-8t54z?file=/src/App.tsx

## おわり

Reactでコンポーネントを作成する時に鉄板となる手法を実例と共に紹介しました。

これらはいずれも主に見た目とロジックを分離して、それぞれの再利用性と高めたり、テストをしやすくしたりするのに役立ちます。

ぜひうまく活用して、見通しのいいフロントエンド開発を楽しみましょう。