React で親コンポーネントのボタンが押されたときに子コンポーネントの処理をしたい
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ 親子を反転させると良さそう
やりたいこと
昔からよく困らせられてる React の問題のひとつが親から子の処理を呼び出したいというものこういうイメージです
const Page = ({ Component }) => {
return (
<div>
<header>
<h1>title</h1>
</header>
<main>
<Component/>
</main>
<footer>
<button onClick={() => { /* call something function */ }}>
BUTTON
</button>
</footer>
</div>
)
}
const Component = () => {
const [state, setState] = useState()
const something = () =>{ /*...*/ }
return (
<div>
...
</div>
)
}
親 (Page) にあるフッターのボタンを押したときの処理で 子 (Component) の something 関数を呼び出したいです
something 関数が 子の中にあるということは 子の中の state などが必要になるということです
子の中の値に依存しないなら外部モジュールにして 親で import して使えば良いですから
解決策は
React 的に推奨されてるらしい方法は state を親で持つものです子で持ってるデータはすべて親でも持っていれば親から something 関数を呼び出せば済みます
基本はこうしているのですが 全部のデータがルート近くのコンポーネントに集まってきますし 扱いづらくなってあまり好きじゃないです
定期的に不満に感じてはなにかいい方法はないかなと考えてたりします
それでこれまでみつかった代替法はというと
- ref 経由で関数呼び出し
- state で通知
- event で通知
- 諦める
というものです
ref
ref を使う方法では親から子に ref を渡して 子では受け取った ref に something 関数を入れます親は ref を経由して something 関数を呼び出します
一見これで良さそうですけど React 的に推奨されない方法らしいです
そう言われると使用するのに少し抵抗があります
これをサポートするための hook も公式にありますがドキュメントでは避けたほうが良いようなことが書かれています
宣言的に書いて命令的に書くべきじゃないというのはわかりますが リスナの処理で関数を呼び出すのは仕方ない部分だと思います
実際 onChange 関数を親から子に渡して呼び出してもらうことは当たり前のように行われていますし
親から子だってボタン配置など UI によってはありえてもおかしくないように思います
少し話がそれますが イベントは全コンポーネントに伝わって各自が必要なら処理する形が良いという考えから 自分で作っていた WebComponents ベースのアプリではそういう仕組みを採用しました
ルートコンポーネントから子コンポーネントに通知させていって 各コンポーネントが必要ならなにかの処理をします
本当にすべての末端コンポーネントまで通知するとムダが多いので 実際には親要素が必要ない場合は子要素への通知を省略したりしてます
これで特に困ったことはなかったですし コンポーネントの場所を問わないので便利でした
そんななので この React の制限はあまり好きじゃないです
state
state を使う方法は 実現はできるものの あまり良い方法とは思えず これなら ref の方が良いかなと思ってます子の関数を呼び出すときに引数として渡したい値を state に保存し props として子に渡します
子では渡された値が変わった場合のみ 関数を実行します
副作用になるので useEffect を使います
useEffect にはちょうど変化を検知する機能もあるので適してます
const Parent = () => {
const [text, setText] = useState("")
const [send_child, setSendChild] = useState(null)
const onClick = () => {
setSendChild({ value: text })
}
return (
<div>
<div>
<input value={text} onChange={(event) => setText(event.target.value)} />
<button onClick={onClick}>BUTTON</button>
</div>
<Child data={send_child} />
</div>
)
}
const Child = ({ data }) => {
useEffect(() => {
if (data) {
console.log("child", data.value)
}
}, [data])
return (
<div>
child
</div>
)
}
連続して同じ引数で呼び出せるように 子に渡す値はオブジェクトでラップして毎回参照が変わるようにしています
親側で input に適当に文字を入れてボタンを押すと子の中の console.log が実行されます
event
親から受け取ったオブジェクトにリスナを設定するというのは自然な処理のひとつだと思うので EventTarget を作って渡すことにします関数を呼び出したいときは 親でイベントを起こし 子でイベントを受け取り処理を行います
const Parent = () => {
const [text, setText] = useState("")
const [target] = useState(new EventTarget())
const onClick = () => {
target.dispatchEvent(new CustomEvent("click-button", { detail: text }))
}
return (
<div>
<div>
<input value={text} onChange={(event) => setText(event.target.value)} />
<button onClick={onClick}>BUTTON</button>
</div>
<Child target={target} />
</div>
)
}
const Child = ({ target }) => {
useEffect(() => {
const fn = eve => console.log(eve.detail)
target.addEventListener("click-button", fn)
return () => target.removeEventListener("click-button", fn)
}, [target])
return (
<div>
child
</div>
)
}
考え方的には良いのかもですが 結構面倒になるのがいまいちなところです
ref の方がシンプルに書けます
諦める
実現はできるものの あまりベストとは言いにくい解決策しかないので いっそ諦めることにします具体的には そういう親子に分かれる作りにはしないというものです
ヘッダーやフッターみたいな離れたところにボタンだけを配置するのが原因なのでそういうことはしません
関連するものはコンポーネント内にまとめてしまいます
もともとウェブがコンポーネントに分かれていなくてページ全体がグローバルなものだったから自由すぎる配置になった結果だと思います
ボタンだけ別コンポーネントにあるような作りにせず コンポーネントにまとまってる方がわかりやすいことも多いのでレイアウトをコンポーネントに合わせて見直します
Context
離れたコンポーネント間での共有といえば Context が思い浮かびますしかし 今回は親子関係なので props を渡すのが辛いなんてことはなく 直接渡すだけで済みます
仮に Context を使っても ref を渡したときのようにしかならないと思うので 解決策にはなりません
親子の反転
長かったですが ここまでは以前からの話でいつも通りです今回ふと 親子を反転すればいいんじゃないと思いついたので その方法を試してみます
一番上の Page と Component の例を使います
元は Page が親で Component が子でした
それを逆にして Component を親にします
こうすれば Page に props でリスナ関数を渡すだけで済みます
const Page = ({ onClickButton }) => {
return (
<div>
<header>
<h1>title</h1>
</header>
<main>
{children}
</main>
<footer>
<button onClick={onClickButton}>BUTTON</button>
</footer>
</div>
)
}
const Component = () => {
const [state, setState] = useState()
const something = () =>{ /*...*/ }
return (
<Page onClickButton={something}>
<div>
...
</div>
</Page>
)
}
思ってたよりもいい感じにできました
この方法でも state を親で持つことになってますが Component で管理したい部分なので問題ないです
Page で管理してる state を Component に渡したいとなれば 今は Page の children として div を直接書いてますが関数にすれば受け取れます
親子を逆にしただけでなので Component 内のイベントの処理時に Page の関数を呼び出したいというケースは同じ問題が出てきます
ですが 双方向に呼び出したいってことはほとんどないですし 逆になれば親で state を持つことが自然に受け入れられるケースが多く感じます
子の関数を呼び出したいと思ったら親子を反転させるのを考えてみるのは良さそうです
とりあえず今のところはこれは一番良さそうな候補です