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のようなトラッキングツールでデータ送信する際のコンポーネントの表示位置に関する情報をコンポーネント包含状態ベースで表現できたりと、色々便利に使えます。