Reactの関数コンポーネントで別々のdefaultPropsを指定する方法


はじめに

ドワンゴでニコニコ生放送のWebフロントエンジニアをやっています misuken です。

今回はReactで1つの関数コンポーネントに別々のdefaultPropsを指定したコンポーネントを作成する方法を紹介します。

defaultPropsはmutableなオブジェクト

Reactで関数コンポーネントのdefaultPropsを使いたいけど、参照が同じだから困ったなぁという経験はありませんか?

例えばこんなこと。

  • 複数箇所で同じコンポーネントに対して別のdefaultPropsを指定したい
  • ReactElementではなく、コンポーネントのままdefaultPropsを指定して何かに渡す必要がある

うっかり以下のようなに2箇所でdefaultPropsを更新してしまうと、最終的にdefaultPropsがどちらか一方になって壊れたりします。

example.tsx
import { VFC } from "react";
const Example: VFC<{ foo?: string; bar?: number }> = ({ foo, bar }) => (
  <div>{foo} {bar}</div>
);

export default Example;
a.tsx
import Example from "./example";

// 同じ参照に対する代入
Example.defaultProps = { foo: "A" };
b.tsx
import Example from "./example";

// 同じ参照に対する代入
Example.defaultProps = { bar: 1 };

関数コンポーネントをクローンする

この問題は関数コンポーネントの参照が同じままだから発生するわけで、以下のように関数の参照を変えることで解決します。

a.tsx
import Example from "./example";

const FooExample = Object.assign({}, Example, { defaultProps: { foo: "A" } });
b.tsx
import Example from "./example";

const FooExample = Object.assign({}, Example, { defaultProps: { bar: 1 } });

便利関数にする

便利な関数にするとこんな感じで、playgroundを見るとわかるように得られるコンポーネントの型も予想通りな感じになります。

import { FC, VFC, ForwardRefExoticComponent as FREC } from "react";
// forwardRefで作ったコンポーネントも対応
type AnyFC<T> = VFC<T> | FC<T> | FREC<T>;

export function cloneFC<T extends Partial<U>, U>(component: FREC<T>, defaultProps?: U & Partial<T>): FREC<T> & { defaultProps: U | undefined };
export function cloneFC<T extends Partial<U>, U>(component: VFC<T>, defaultProps?: U & Partial<T>): VFC<T> & { defaultProps: U | undefined };
export function cloneFC<T extends Partial<U>, U>(component: FC<T>, defaultProps?: U & Partial<T>): FC<T> & { defaultProps: U | undefined };
export function cloneFC<T extends Partial<U>, U>(
  component: AnyFC<T>,
  defaultProps?: U & Partial<T>,
): AnyFC<T> & { defaultProps: U | undefined } {
  return Object.assign(
    {},
    component,
    defaultProps && {
      // 元々持っていたdefaultPropsを引き継ぎつつ新しいもので上書き
      defaultProps: { ...component.defaultProps, ...defaultProps },
    },
  );
}

当たり前のことですが、クローンした後はdisplayNameを書き換えても元のコンポーネントに影響を与えません。

おわりに

アーキテクチャにもよると思うのですが、開発中に一つのコンポーネントを複数箇所で別々のdefaultPropsを代入できると便利だなと思うことが度々あり、試してみたらあっさりできたのでこれから活用していこうかなと考えています。

同じようなやり方はすでにやられてそうだなと思ったのですが、ちょっと調べてみても見つからなかったので、もしこのやり方で何か弊害などある場合は教えていただけると幸いです。