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を使って共有します。
サンプル
ol
とli
要素をラップし、最後にクリックされた子コンポーネントの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コンポーネントに対して、例えばfetch
やuseSelector
で得た値を渡したり、イベントハンドラにfetch
やuseDispatch
などを使った関数を渡したりします。
サンプル
ある項目のリストを表示する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でコンポーネントを作成する時に鉄板となる手法を実例と共に紹介しました。
これらはいずれも主に見た目とロジックを分離して、それぞれの再利用性と高めたり、テストをしやすくしたりするのに役立ちます。
ぜひうまく活用して、見通しのいいフロントエンド開発を楽しみましょう。