Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

型安全に React の Provider をマージしてネストを浅くしたい

苦節半年くらいしてようやく実現できてメッチャうれしいので記事も書くぞ!!!

Context のネストがつらい

React の Context、いくつも書くとなるとネストがどんどん深くなっていってつらいですよね。

<ContextA.Provider>
  <ContextB.Provider>
    <ContextC.Provider>
      <ContextD.Provider>
        <ContextE.Provider>
          ...
        </ContextE.Provider>
      </ContextD.Provider>
    </ContextC.Provider>
  </ContextB.Provider>
</ContextA.Provider>

これ、フラットにしたくありませんか?

<MergedProvider>
  ...
</MergedProvider>

今回はこれが実現できたので紹介します!!!! (※実際のインターフェイスはこうではありませんが、まあ概ねこれっぽい感じです)

コード

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript

import { FC, Context, ReactNode, createContext } from 'react';
type IsProvider<T> = T extends (infer R)[] ? R extends [Context<infer x>, infer y] ? x extends y ? JSX.Element : void : void : void;

const mergeProvider = <T extends (readonly [] | readonly any[])[]>(contexts: T, children: ReactNode): IsProvider<T> => {
    const res = contexts.reduce((acc, [Context, value]) => {
        return <Context.Provider value={value}>{acc}</Context.Provider>;
    }, children);
    return res as IsProvider<T>;
}

const NumContext = createContext(0);
const StringContext = createContext('string');

const App: FC = () => {
  return mergeProvider([
    [NumContext, 0],
    [StringContext, 'string']
  ], <div />);
}

こんな感じです。

invalid な contexts が入ってきた場合、megeProvider は void を返してくれるため、型検査が通りません。やったね。

const App: FC = () => {
  return mergeProvider([
    [NumContext, '0'], // invalid なので型検査が通らない
    [StringContext, 'string']
  ], <div />);
}

仕組み

仕組みとしてはごく単純で、contexts はなんでも受け入れておき、処理も context と value のタプルが渡されてきたと想定して書きました。当然このままだと実行時エラーがおきます(なんでも受け入れてるので)。しかし、正常な組み合わせ以外が渡された場合、型の返り値が void となるため、結果として整合性が取れます。

全く正しくない引数を受け入れるという発想ができるまですごく時間がかかりました…