Reactのprops drilling(バケツリレー)とhooksに我々はどう立ち向かっていけばよいのか
最近Reactに入門しました。
アプリケーションの規模が大きくなってくると問題になってくる一つにprops drilling(バケツリレー)と呼ばれるものがあります。
今回はprops drilling(バケツリレー)が“つらい”と思ってから、どのように立ち向かっていったのか一例をご紹介します。
INDEX
Reactのprops drilling(バケツリレー)が“つらい”
Reactコンポーネントの引数にPropsを渡す方法では、親子関係にあるコンポーネントのツリー構造の間を飛ばして渡すことができません。
※ただし、Context API や Redux 等のステート管理ライブラリを使えば可能です。
親、子、孫、ひ孫とコンポーネントツリー構造が深くなってくると、ひ孫コンポーネントでのみ必要なPropsなのに、子・孫コンポーネントではただ下のコンポーネントにPropsを渡すためだけにProps定義することがしばしば発生します。
// 親コンポーネント
export const ParentComponent = (): JSX.Element => {
const [count, setCount] = useState<number>(0);
useEffect(() => {
fetch("/api")
.then((res) => res.json())
.then((data) => {
setCount(data.count);
});
return () => {
// cleanup
};
}, []);
return (
<div>
<p>親コンポーネント</p>
<ChildComponent count={count} setCount={setCount} />
</div>
);
}
// 子コンポーネント
type ChildProps = {
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}
export const ChildComponent = ({ count, setCount }: ChildProps): JSX.Element => {
return (
<div>
<p>子コンポーネント</p>
<GrandChildComponent count={count} setCount={setCount} />
</div>
);
}
// 孫コンポーネント
type GrandChildProps = {
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}
export const GrandChildComponent = ({ count, setCount }: GrandChildProps): JSX.Element => {
return (
<div>
<p>孫コンポーネント</p>
<GreatGrandChildComponent count={count} setCount={setCount} />
</div>
);
}
// ひ孫コンポーネント
type GreatGrandChildProps = {
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}
export const GreatGrandChildComponent = ({ count, setCount }: GreatGrandChildProps): JSX.Element => {
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>ひ孫コンポーネント</p>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
上記は極端な例ですが、ひ孫コンポーネントで必要なステートと更新用関数をProps経由で渡しています。
ひ孫で必要なPropsの変更に親、子、孫、ひ孫と複数のコンポーネントに修正が必要になったり、TypeScriptの型定義が冗長になったりとつらいです。
children Props , component Props
こういった時に役立つのが公式のドキュメントにも記載されているCompositionです。
先程の例を書き直してみます。
// 親コンポーネント
export const ParentComponent = (): JSX.Element => {
const [count, setCount] = useState<number>(0);
useEffect(() => {
fetch("/api")
.then((res) => res.json())
.then((data) => {
setCount(data.count);
});
return () => {
// cleanup
};
}, []);
return (
<div>
<p>親コンポーネント</p>
<ChildComponent>
<GrandChildComponent>
<GreatGrandChildComponent
count={count}
setCount={setCount}
/>
</GrandChildComponent>
</ChildComponent>
</div>
);
}
// 子コンポーネント
type ChildProps = {
children: React.ReactNode;
}
export const ChildComponent = ({ children }: ChildProps): JSX.Element => {
return (
<div>
<p>子コンポーネント</p>
{children}
</div>
);
}
// 孫コンポーネント
type GrandChildProps = {
children: React.ReactNode;
}
export const GrandChildComponent = ({ children }: GrandChildProps): JSX.Element => {
return (
<div>
<p>子コンポーネント</p>
{children}
</div>
);
}
// ひ孫コンポーネント
type GreatGrandChildProps = {
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}
export const GreatGrandChildComponent = ({
count,
setCount,
}: GreatGrandChildProps): JSX.Element => {
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>ひ孫コンポーネント</p>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
子、孫コンポーネントからひ孫コンポーネントでしか使わないPropsを無くすことができました。
複数のコンポーネントを指定した位置に表示させたい時にはPropsにコンポーネントを渡すこともできます。
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
※React公式ドキュメントより引用
ロジックが散らばって“つらい”
最初の例ではステートの更新用関数をPropsで渡して、実際の更新処理はひ孫のコンポーネントで定義していました。
export const GreatGrandChildComponent = ({ count, setCount }: GreatGrandChildProps): JSX.Element => {
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>ひ孫コンポーネント</p>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
※再掲
同じステートをincrement, decrementしたい別のコンポーネントが必要になった場合に、都度更新用関数をコピペして作らなければならなくなります。
こういったことを防ぐために、なるべくステート定義と同じモジュール内でステート更新用関数を定義しておきたいです。
// 親コンポーネント
export const ParentComponent = (): JSX.Element => {
const [count, setCount] = useState<number>(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
useEffect(() => {
fetch("/api")
.then((res) => res.json())
.then((data) => {
setCount(data.count);
})
return () => {
// cleanup
};
}, []);
return (
<div>
<p>親コンポーネント</p>
<ChildComponent>
<GrandChildComponent>
<GreatGrandChildComponent
count={count}
increment={increment}
decrement={decrement}
/>
</GrandChildComponent>
</ChildComponent>
</div>
);
}
コンポーネントのPropsが増えると“つらい”
Props childrenを使ってprops drilling(バケツリレー)を解決しても新たな問題が出てきたりします。
export const ParentComponent = (): JSX.Element => {
...
return (
<ChildComponent
count={count}
increment={increment}
decrement={decrement}
double={double}
reset={reset}
>
<GrandChildComponent
count={count}
increment={increment}
decrement={decrement}
double={double}
reset={reset}
>
<GreatGrandChildComponent
count={count}
increment={increment}
decrement={decrement}
double={double}
reset={reset}
/>
</GrandChildComponent>
</ChildComponent>
);
}
これもまた極端な例ですが、複数のステートや更新用関数を扱うようになってくるとコンポーネントの引数が増えていってつらいです。
カスタムフックでステートと更新操作をまとめる
カスタムフックでステートと更新操作をオブジェクトにまとめてしまいます。
type State = {
count: number;
};
type Counter = State & {
increment: () => void;
decrement: () => void;
double: () => void;
reset: () => void;
};
const useCounter = (initialState: State["count"] = 0): Counter => {
const [count, setCount] = useState<State["count"]>(initialState);
const increment = () => {
setCount(count + 1);
}
const decrement = () => {
setCount(count - 1);
}
const double = () => {
setCount(count * 2);
}
const reset = () => {
setCount(0);
}
return {
count,
increment,
decrement,
double,
reset,
};
}
カスタムフックでオブジェクトにステートと更新用関数をまとめることで、カスタムフックを使うコンポーネントを見るとだいぶスッキリします。
export const App = (): JSX.Element => {
const counter = useCounter();
return (
<ChildComponent counter={counter}>
<GrandChildComponent counter={counter}>
<GreatGrandChildComponent counter={counter} />
</GrandChildComponent>
</ChildComponent>
);
}
Propsで渡された子コンポーネントでの書き方はこんな感じになります。
type GreatGrandChildProps = {
count: number;
increment: () => void;
decrement: () => void;
double: () => void;
reset: () => void;
}
export const GreatGrandChildComponent = ({
count,
increment,
decrement,
double,
reset,
}: GreatGrandChildProps): JSX.Element => {
return (
<div>
<p>ひ孫コンポーネント</p>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={double}>x2</button>
<button onClick={reset}>reset</button>
</div>
);
}
さいごに
props drilling(バケツリレー)、ロジックが散らばる、コンポーネントのPropsが増えてきてつらいのを経験して、考えた末こういった形に一旦落ち着きました。
PHPer脳ではデータ(ステート)と手続き(更新用関数)が一箇所にまとまっているこのような形式がPHPのclassを使ったオブジェクトのような形で見通しがいいように思えるのですが、みなさんどうしているんでしょうか。
この形式が最適解ではないと思っていますが、現在進行形でprops drilling(バケツリレー)がつらいと思っている方の一助になるとうれしいです。
この記事を書いた人
- 2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー