63
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-09-08

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

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

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

Render props

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

サンプル

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

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

実用的な例

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

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

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

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

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となったら結果を表示しています。

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

Higher-order components

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

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

サンプル

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

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

実用的な例

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

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

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を使って共有します。

サンプル

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

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

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>
  );
}

Container components

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

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

サンプル

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

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} />;
}

おわり

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

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

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

63
58
5

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
63
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?