技術と魚

技術屋ですが経営もやってます

React.Contextのネストを使ったテクニック

I ❤️ Context. 2020年が終わるまでにプロダクションからReduxのコードを消そうと思い、必死にReact.Contextへ移行していました。

さてContextについて面白いテクニックを見出したので紹介します。Contextはネストできます。これを使うことで、コンポーネントの包含によって情報を累積的に構築し、renderされる任意のコンポーネント上で包含状態に基づいて構築された情報を利用することで、様々なケースで便利に使えます。日本語って難しい。具体例を考えます。

ある3次元情報があり、それをNxMのtable L個で表示します。ユーザがtableのセルをクリックしたら、その情報が3次元空間上のどの点のものかを出力するコンポーネントを考えます。

const data = [1,2,3,4,5].map((x) =>
  [1,2,3,4,5].map((y) =>
    [1,2,3,4].map((z) =>
      x * y + z * z
    )
  )
);

export default function App() {
  return data.map((mat, x) =>
    <table key={x} border="1">
      {mat.map((vec, y) =>
        <tr key={y}>
          {vec.map((val,z) =>
            <td key={z}>{val}</td>
          )}
        </tr>
      )}
    </table>
  );
}

まあこの状態だったら、x,y,zを使えばいいんですが、開発をしているとTable, Row, Cellが個別のコンポーネントとして進化し、そこに対してx,y,zの値をprops経由で渡していくと、徐々にバケツリレーが大きくなり、辛くなってきます。

const Cell = ({ val, x, y, z }) => (
  <td onClick={() => console.log([x, y, z])}>{val}</td>
);

const Row = ({ vec, x, y }) => (
  <tr>
    {vec.map((val, z) => (
      <Cell val={val} x={x} y={y} />
    ))}
  </tr>
);

const Table = ({ mat, x }) => (
  <table border="1">
    {mat.map((vec, y) => (
      <Row vec={vec} x={x} y={y} key={y} />
    ))}
  </table>
);

export default function App() {
  return data.map((mat, x) => <Table mat={mat} x={x} key={x} />);
}

ここで、Contextを使うことで情報伝達することを考えます。まず、 createContextでvalueの初期値として空集合や空リストを表現し、次にそれをconsumeして何かを追加したものをprovideするだけのcomponentを作成します(以下の例ではAddPath)。最後にuseContextで取り出します。

const Context = React.createContext({});
const AddPath = ({ children, ...props }) => {
  const val = React.useContext(Context);
  return (
    <Context.Provider value={{ ...val, ...props }}>{children}</Context.Provider>
  );
};

const Cell = ({ val }) => {
  const { x, y, z } = React.useContext(Context);
  return (
    <td onClick={() => console.log([x, y, z])}>{val}</td>
  );
}

const Row = ({ vec }) => (
  <tr>
    {vec.map((val, z) => (
      <AddPath z={z} key={z}>
        <Cell val={val} />
      </AddPath>
    ))}
  </tr>
);

const Table = ({ mat }) => (
  <table border="1">
    {mat.map((vec, y) => (
      <AddPath y={y} key={y}><Row vec={vec} /></AddPath>
    ))}
  </table>
);

export default function App() {
  return data.map((mat, x) => <AddPath x={x} key={x}><Table mat={mat} /></AddPath>);
}

こうすることで、propsを流していくことなく、末端のコンポーネントがAddPathによってどのように包含されているか、に相当する情報が得られます。ここでは集合表現をobjectにしていますが、もちろんStackにしたりすることでより正確にできますし、Setにすれば重複も無くせます。

これを使うことで、例のような状況だけでなく、例えばmixpanelのようなトラッキングツールでデータ送信する際のコンポーネントの表示位置に関する情報をコンポーネント包含状態ベースで表現できたりと、色々便利に使えます。