😙

Propのバケツリレー地獄(=Prop Drilling)を防ぐReact Context

2021/09/22に公開

Reactでpropに役立つReact Contextについて、便利な点や使い方をまとめ、実装まで行った。

概要

問題点

React Contextを使わず、propにデータを渡して受け取って、渡して受け取って、、、としていると階層が深いと何を渡しているのか分からなくなる。
例えば、下図のように緑色のbackgroundにあるデータ'languages'をcountainer3に渡したい時、React Contextを使う場合と使わない場合の二つのパターンで実装することができる。

パターン1(React Contextを使う場合)

パターン2(React Contextを使わない場合)

パターン2のReact Contextを使わない場合は、Background → Container1 → Container2 → Container3の順にデータが渡っている。データはContainer3でしか使わないにも関わらず、Propをバケツリレーのように渡している。
これをコードにすると以下のような感じ。

app.js
import "./App.css";

function App() {
  const languages = ["JavaScript", "Python", "Java", "Golang"];
  return (
    <div className="background">
      Background 
      <p className="languages">
        Pass languages[JavaScript, Python, Java, Golang] to Container 3
      </p>
      {/* languages ->  Container1*/}
      <Container1 languages={languages} />
    </div>
  );
}

export default App;

function Container1({ languages }) {
  return (
    <div className="container1">
      Container 1
      <Container2 languages={languages} />
      {/* Container1 ->  Container2 */}
    </div>
  );
}

function Container2({ languages }) {
  return (
    <div className="container2">
      Container 2
      <Container3 languages={languages} />
      {/* Container2 ->  Container3 */}
    </div>
  );
}

function Container3({ languages }) {
  return (
    <div className="container3">
      Container 3
      <ul className="languages-area">
        {languages.map((language, i) => (
          <li key={i}>{language}</li>
        ))}
      </ul>
    </div>
  );
}

Container1やContainer2では、実際にデータを使わないにも関わらず、データを受け取り、Container3までデータを運んでいる。
これだとできないことはないが、コンポーネントが10階層にもなっていた場合、もはや何のデータを渡しているのか、もらっているのか分からなくなる。

解決策

そこでReact Contextを使って、BackgroundからComponent3に直接データを渡す。
使い方は簡単で、React Contextのイメージはこんな感じ。

AさんとCさん共通の友達Bさんをcreateして、Bさんを通じてデータを渡したり受け取ったりするイメージ。

実装

1. react contextをインポート

まず、react contextをインポートする。
その後、createContext()を使って、LanguageContextというcontextを宣言。
↑ここで作ったものがいわゆるAさんとCさんの共通の友達Bさんで、データを受け取ったり、渡したりをしてくれるイメージ。

app.js
import "./App.css";
 {/* 下記二つ */}
import { createContext, useContext } from "react";

const LanguageContext = createContext();

2. データをcontextに渡す

Provider (=提供する側)を使って、渡したいデータを先ほど作ったLanguageContextに渡す。
共通の友達Bさんにデータを渡すイメージ。

注意点

・LanguageContextに渡したデータを受け取れる範囲は、LanguageContextで囲った中だけ
・LanguageContext.Providerの中に書く値は'value'にしなければならない

app.js
import "./App.css";
 {/* 下記二つ */}
import { createContext, useContext } from "react";

const LanguageContext = createContext();

 {/* ここから下を追加 */}
function App() {
  const languages = ["JavaScript", "Python", "Java", "Golang"];
  return (
    <LanguageContext.Provider value={{ languages }}>
      <div className="background">
        Background 
        <p className="languages">
          Pass languages[JavaScript, Python, Java, Golang] to Container 3
        </p>
        <Container1 />
      </div>
    </LanguageContext.Provider>
  );
}

3. データの受け渡しに関係ない部分の記述

propを受け取ったり、渡したりをしなくていい分、コードがスッキリする。

app.js
import "./App.css";
 {/* 下記二つ */}
import { createContext, useContext } from "react";

const LanguageContext = createContext();

function App() {
  const languages = ["JavaScript", "Python", "Java", "Golang"];
  return (
    <LanguageContext.Provider value={{ languages }}>
      <div className="background">
        Background 
        <p className="languages">
          Pass languages[JavaScript, Python, Java, Golang] to Container 3
        </p>
        <Container1 />
      </div>
    </LanguageContext.Provider>
  );
}
 {/* ここから下を追加 */}
function Container1() {
  return (
    <div className="container1">
      Container 1
      <Container2 />
    </div>
  );
}

function Container2() {
  return (
    <div className="container2">
      Container 2
      <Container3 />
    </div>
  );

4. データをcontextから受け取る

データを受け取りたいコンポーネントの部分(今回はContainer3)で、useContext()を使って、データを受け取る。
useContext()の中には、context = LanguageContextを入れて,
useContext(LanguageContext)とする。すると、Providerが渡したデータを受け取れる。

app.js
import {  useContext } from "react";
  
function Container3() {
  const ctx = useContext(LanguageContext);
  console.log(ctx.languages);
}

すると、しっかりとデータを受け取ることができている。

app.js
["JavaScript", "Python", "Java", "Golang"];

5. 受け取ったデータを使う

ここまで来れば、propで受け取ったのと同じようにデータを使うだけ。

app.js
import "./App.css";
 {/* 下記二つ */}
import { createContext, useContext } from "react";

const LanguageContext = createContext();

function App() {
  const languages = ["JavaScript", "Python", "Java", "Golang"];
  return (
    <LanguageContext.Provider value={{ languages }}>
      <div className="background">
        Background 
        <p className="languages">
          Pass languages[JavaScript, Python, Java, Golang] to Container 3
        </p>
        <Container1 />
      </div>
    </LanguageContext.Provider>
  );
}

function Container1() {
  return (
    <div className="container1">
      Container 1
      <Container2 />
    </div>
  );
}

function Container2() {
  return (
    <div className="container2">
      Container 2
      <Container3 />
    </div>
  );
  
 {/* ここから下を追加 */}
function Container3() {
  const ctx = useContext(LanguageContext);
  console.log(ctx.languages);
  return (
    <div className="container3">
      Container 3
      <ul className="languages-area">
        {ctx.languages.map((language, i) => (
          <li key={i}>{language}</li>
        ))}
      </ul>
    </div>
  );
}

補足

本来であれば、storeというフォルダを作成して、その中にLanguageContext.js等のファイルを作る。そしてcontextを作成してエクスポートをする方がいいそうだが、今回は見やすさ重視のため、同じファイルに全て記載している。

LanguageContext.js

import { createContext } from "react";

const LanguageContext = createContext();

export default LanguageContext

まとめ

CSS含め、今回のコードはGitHubにも。

Discussion