見出し画像

useContextを使用したReact Compound Patternについて

こんにちわ。
スペースマーケットでFEチームのEMやっている成原です。
子供が言葉をよく覚えるようになってきました。
最近は「ホワタァー!!!!」という言葉を連呼しています。
こいつは強い子に育ちそうだ。
ちなみに娘です。

さて本日はReactのデザインパターン、 Compound Patternについて話します。

Compound Patternについて

Compound Patternは数年前から使用されているデザインパターンです。
1つの文脈内で関連する複数のコンポーネントを実装する際に用いられます。
こちらのパターンを使用することで以下のメリットが得られます。

・柔軟なコンポーネント構造の提供
・暗黙的な状態の共有

と説明してもピンと来ないと思うので、カスタムしたselect / optionタグを実例として説明します。

まずカスタムしたselect / optionタグ(以下MySelect,MyOption)の要件は以下とします。

・ MySelectはvalue propsを任意で受け取る
・MyOptionはvalue propsを必須で受け取る
・ MyOptionに渡されたvalueを一致するvalueを保持するMyOptionはselected状態となる

Compound Patternを使用しようせず、実装した場合、以下のコンポーネント構造になります。

<MySelect
 value={1}
 options={[
   {
     label: "hoge",
     value: 1,
   },
   {
     label: "fuga",
     value: 2,
   },
 ]}
/>

MySelectのみを外部露出させ、MyOptionに渡す値をoptionsという配列で渡すパターンです。
まずHTML標準のselect / option構造から解離してしまっていますね。
また、MySelectが保持しているMyOptionに渡すpropsが少ない場合は良いですが、他にも渡したくなった場合はどうなるでしょうか?
おそらく、options配列内部のオブジェクトプロパティを増やすことになりますが、早々に破綻する未来が見えます。

// 確約された破綻した未来
<MySelect
 value={1}
 options={[
   {
     label: "hoge",
     value: 1,
     propsA: false,
     propsB: true,
     propsC: 1,
   },
   {
     label: "fuga",
     value: 2,
     propsA: false,
     propsB: true,
     propsC: 1,
   },
 ]}
/>

一方Compound Patternを使用した場合、以下のコンポーネント構造になります。

<MySelect value={1}>
 <MyOption value={1}>hoge</MyOption>
 <MyOption value={2}>fuga</MyOption>
</MySelect>

非常にスマートな構造になっていることがわかります。
またCompound Pattern未使用時の以下デメリットも解決されています。

・HTML標準のselect / option構造からの解離
・MyOptionに渡すpropsが増えた場合の柔軟性

と言う訳でCompound Patternのメリットを示せたところで、useContextを使用した具体的な実装方法を話します。

useContextを使用したCompoundパターン

type SelectProps = Pick<
 React.ComponentProps<"select">,
 "value"
> & {
 children: React.ReactElement[];
};

const SelectContext = React.createContext<SelectProps["value"]>("");

const useSelectContext = () => {
 const selectedValue = useContext(SelectContext);
 if (!selectedValue) {
   throw new Error(
     `useSelectContextはMySelectコンポーネントでwrapしていないコンポーネントでは使用できません`
   );
 }
 return selectedValue;
};
const MySelect: VFC<SelectProps> = ({ value, children }) => {
 return (
   <SelectContext.Provider value={value}>
     <select value={value}>
       {children}
     </select>
   </SelectContext.Provider>
 );
};

type OptionProps = Pick<
 React.ComponentProps<"option">,
 "value" | "disabled"
> & {
 children: React.ReactNode;
};

const MyOption: VFC<OptionProps> = ({ value, children }) => {
 const selectedValue = useSelectContext();
 const selected = useMemo(() => value === selectedValue, [
   value,
   selectedValue
 ]);
 return (
   <option selected={selected} value={value}>
     {children}
   </option>
 );
};

いったん1つのファイルに書いていますが、実際には適宜分解するのが良いでしょう。
では、各部分の説明をしてゆきます。

Context API部分

const SelectContext = React.createContext<SelectProps["value"]>("");

const useSelectContext = () => {
 const selectedValue = useContext(SelectContext);
 if (!selectedValue) {
   throw new Error(
     `useSelectContextはMySelectコンポーネントでwrapしていないコンポーネントでは使用できません`
   );
 }
 return selectedValue;
};

まずReact.createContextでコンテキストオブジェクトを作成します。
次にコンテキストオブジェクトの現在地を提供するカスタムフックを作成しています。
このようにカスタムフック格納することで、コンテキストオブジェクトの存在を隠蔽しています。

MySelect部分


const MySelect: VFC<SelectProps> = ({ value, children }) => {
 return (
   <SelectContext.Provider value={value}>
     <select value={value}>
       {children}
     </select>
   </SelectContext.Provider>
 );
};

MySelect内部でプロバイダーコンポーネントに値を渡しています。
これによりMySelectの子要素で、渡された値を購読可能な状態になります。

MyOption部分

const MyOption: VFC<OptionProps> = ({ value, children }) => {
 const selectedValue = useSelectContext();
 const selected = useMemo(() => value === selectedValue, [
   value,
   selectedValue
 ]);
 return (
   <option selected={selected} value={value}>
     {children}
   </option>
 );
};

MyOptionでは、上で定義したuseSelectContextからMySelectに渡された値を購読しています。

これらの実装により、以下のような標準要素に準じたコンポーネント構造を実現できます。

<MySelect value={selectedValue}}>
 <MyOption value={1}>1</MyOption>
 <MyOption value={2}>2</MyOption>
</MySelect>

まとめ

おわかりいただけたでしょうか?
このように、親子関係があり、且つ暗黙的な状態共有が存在する2つ以上のコンポーネントではCompound Patternが効果を発揮します。
ですので、似たような場面に遭遇した場合は是非使用してみてください。
さて、弊社スペースマーケットは現在バックエンドエンジニア積極採用中です!
気になった方は以下のリンクから応募お待ちしております!!

最後に屋上を借りられるスペースを共有しておきます。
屋上っていいですよね。
天気のいい日にノーパソ片手、屋上で開発とかいいのではないでしょうか?
(絶対良くない)

#スペースマーケット #frontend #react #typescript

この記事が気に入ったらサポートをしてみませんか?