React で親から子コンポーネントを扱う方法
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ WebComponent 風にメソッド呼び出ししたい
◆ 子コンポーネントで状態を保持してそれを取得
◆ またはフォームのリセットや自動計算やバリデーションなどの何かの処理を行う命令
◆ ref や hook でできなくもない
◆ 工夫すれば React 推奨の方法でも近い形で書けた
◆ 子コンポーネントで状態を保持してそれを取得
◆ またはフォームのリセットや自動計算やバリデーションなどの何かの処理を行う命令
◆ ref や hook でできなくもない
◆ 工夫すれば React 推奨の方法でも近い形で書けた
最近 React でいくつかページを作ることがあって 意外な不便なところだったり これどうすればいいのと思うところがあったので それらをまとめてたのですが 長くなったのでいくつかの記事に分けます
数記事 React 関係が続きます
この辺は WebComponent の方がいいなと思うことが今でも多いです
関数コンポーネントでも ref を受け取り 子コンポーネント側で current に関数をセットし 親が呼び出すことで使えます
Child コンポーネントはボタンを押すとカウントアップします
Parent コンポーネント内のボタン操作で Child コンポーネントのカウントをリセットしたり カウントを取得したりしています
とりあえず問題なく動いてはいますが 少し特殊な方法ですし 推奨されない方法らしいです
シンプルなものだと問題なさそうですが 複雑なものになったときに意図しない問題が起きそうなので 積極的に使うか考えどころです
ダイアログなど表示状態を管理する系のコンポーネントを alert 関数みたいに呼び出すだけにしたくて以前作ったものはこの方法でした
個人的にはこっちのほうが好きですが フックをコンポーネントのように扱うのはどうなんでしょう
コンポーネントと違って直接関数呼び出しになるので コンポーネントの再レンダリングでは確実に呼び出されます
useFoo 内の setState が起きると Component の再レンダリングが発生します
このあたりはコンポーネント化するほうが処理を抑えられてパフォーマンスは優れてそうです
ですが useFoo に分けずに そのまま Component の中に useState などを書くのと同じです
分けたほうが 独立した機能が別にまとまって見やすいですし 別のコンポーネントからも使えます
コンポーネントほどではないにしろ useFoo に分けるデメリットはないように思います
コンポーネント化するほどでないのであればこれでも良さそうです
もう少しありがちな form を例にします
まず フックを使った場合です
App の OK ボタンで form の値を取得して fetch で送信します
App 側では form 要素の配置場所の指定と form の関数を呼び出すだけで済んでいます
次は React らしいやり方にしてみます
Form では状態を持たず useState は使いません
毎回親から受け取ったものを使っています
App 側では useForm が useState になりました
ほとんど変わりなかったです
リセットなどの処理を行いたいときは メソッドを呼び出さなくても App 側で form の状態を持ってるのでそれを変えるだけです
とは言っても そのロジックを App に書きたくないです
今は form 一つだけですが 似たようなものが 5 つ 6 つと出てくると App コンポーネントの定義が複雑化します
リセット時の値を一つずつ指定したり フォーム内の特定の値を自動計算で求めたりしたいとかは form のコンポーネントでやってほしいものです
でもこれは単純にそのモジュールにロジックを入れるだけで解決できそうです
Form.js で名前付きエクスポートでコンポーネント以外のロジックをエクスポートしておきます
あとは App.js で
これなら React のやり方でも困ることはなさそうに見えます
ただ vaildation とか フォーム内の summary タグみたいなものの開き閉じなど コンポーネントのメソッドを呼びたいようなところもあるんですよね
そういうのもすべて state として保持すべきというのが React のやり方なんでしょうけど そんなものまで親で保持するのはなぁと思うところもあります
あと App の state が更新されることになるので Form 内の input イベントで App が毎回レンダリングされます
Form 以外にコンポーネントがいくつかあるとそれらもレンダリングされ パフォーマンスに影響するかもしれません
それらのコンポーネントが Form のデータを使うなら再レンダリングされるべきですが そうでないなら無駄なので props が同じだったら再レンダリングしないように React.memo しておくほうがよいかもです
数記事 React 関係が続きます
WebComponent 風に
React の考え方的に 基本的には子コンポーネントで状態を管理して それを親が任意のタイミングで取得したり 子コンポーネントのメソッドを呼び出したりができませんこの辺は WebComponent の方がいいなと思うことが今でも多いです
ref
ただ完全に不可能というわけでもなく クラスコンポーネントならインスタンスがあるので ref を使って参照できるはずです関数コンポーネントでも ref を受け取り 子コンポーネント側で current に関数をセットし 親が呼び出すことで使えます
const Child = ({ fn_ref }) => {
const [state, setState] = useState({ count: 0 })
fn_ref.current = {
getValue: () => state,
reset: () => setState({ count: 0 })
}
return (
<button onClick={() => setState({ count: state.count + 1 })}>{state.count}</button>
)
}
const Parent = () => {
const ref = useRef()
const show = () => {
alert(ref.current.getValue().count)
}
const reset = () => {
ref.current.reset()
}
return (
<div>
<Child fn_ref={ref} />
<button onClick={show}>show alert</button>
<button onClick={reset}>reset</button>
</div>
)
}
Child コンポーネントはボタンを押すとカウントアップします
Parent コンポーネント内のボタン操作で Child コンポーネントのカウントをリセットしたり カウントを取得したりしています
とりあえず問題なく動いてはいますが 少し特殊な方法ですし 推奨されない方法らしいです
シンプルなものだと問題なさそうですが 複雑なものになったときに意図しない問題が起きそうなので 積極的に使うか考えどころです
hook
フック化する方法もありますダイアログなど表示状態を管理する系のコンポーネントを alert 関数みたいに呼び出すだけにしたくて以前作ったものはこの方法でした
const useFoo = () => {
const [state, setState] = useState(0)
return [
<button onClick={() => setState(state + 1)}>{state}</button>,
{
getValue: () => state,
reset: () => setState(0),
}
]
}
const Component = () => {
const [elem, methods] = useFoo()
const show = () => {
alert(methods.getValue())
}
const reset = () => {
methods.reset()
}
return (
<div>
{elem}
<button onClick={show}>show alert</button>
<button onClick={reset}>reset</button>
</div>
)
}
個人的にはこっちのほうが好きですが フックをコンポーネントのように扱うのはどうなんでしょう
コンポーネントと違って直接関数呼び出しになるので コンポーネントの再レンダリングでは確実に呼び出されます
useFoo 内の setState が起きると Component の再レンダリングが発生します
このあたりはコンポーネント化するほうが処理を抑えられてパフォーマンスは優れてそうです
ですが useFoo に分けずに そのまま Component の中に useState などを書くのと同じです
分けたほうが 独立した機能が別にまとまって見やすいですし 別のコンポーネントからも使えます
コンポーネントほどではないにしろ useFoo に分けるデメリットはないように思います
コンポーネント化するほどでないのであればこれでも良さそうです
React のやり方でも
そう考えていてコンポーネントみたいなフックを気軽に作ろうとしていたのですが よく考えると React の方法でもあまり変わらない気がしてきましたもう少しありがちな form を例にします
まず フックを使った場合です
const useForm = (init_data) => {
const [form, setForm] = useState(init_data || {
name: "",
mail: "",
})
const update = (name) => event => {
setForm({ ...form, [name]: event.target.value })
}
return [
(
<div class="flex-column">
<div>name</div>
<input value={form.name} onChange={update("name")} />
<div>mail</div>
<input value={form.mail} onChange={update("mail")} />
</div>
),
{
getValue: () => form,
something: () => {},
}
]
}
const App = () => {
const [form, form_actions] = useForm(init_data)
const submit = async () => {
const value = form_actions.getValue()
await fetch(...)
// ...
}
return (
<div>
<div>{form}</form>
<button onClick={submit}>OK</button>
</div>
)
}
App の OK ボタンで form の値を取得して fetch で送信します
App 側では form 要素の配置場所の指定と form の関数を呼び出すだけで済んでいます
次は React らしいやり方にしてみます
const Form = ({ form, onChange }) => {
const update = (name) => event => {
onChange({ ...form, [name]: event.target.value })
}
return (
<div class="flex-column">
<div>name</div>
<input value={form.name} onChange={update("name")} />
<div>mail</div>
<input value={form.mail} onChange={update("mail")} />
</div>
)
}
const App = () => {
const [form, setForm] = useState(init_data || {
name: "",
mail: "",
}))
const submit = async () => {
const value = form
await fetch(...)
// ...
}
return (
<div>
<div><Form form={form} onChange={setForm} /></form>
<button onClick={submit}>OK</button>
</div>
)
}
Form では状態を持たず useState は使いません
毎回親から受け取ったものを使っています
App 側では useForm が useState になりました
ほとんど変わりなかったです
リセットなどの処理を行いたいときは メソッドを呼び出さなくても App 側で form の状態を持ってるのでそれを変えるだけです
とは言っても そのロジックを App に書きたくないです
今は form 一つだけですが 似たようなものが 5 つ 6 つと出てくると App コンポーネントの定義が複雑化します
リセット時の値を一つずつ指定したり フォーム内の特定の値を自動計算で求めたりしたいとかは form のコンポーネントでやってほしいものです
でもこれは単純にそのモジュールにロジックを入れるだけで解決できそうです
Form.js で名前付きエクスポートでコンポーネント以外のロジックをエクスポートしておきます
export default () => {
// Form Component
}
export const initForm = () => {
return {
// default value
}
}
export const calcForm = (form) => {
return {
...form,
z: form.x + form.y,
}
}
あとは App.js で
import Form, { initForm, calcForm } from "./Form.js"
const App = () => {
const [form, setForm] = useState(initForm)
const onClickCalc = () => {
setForm(calcForm(form))
}
return (
<div>
<div><Form form={form} onChange={setForm} /></form>
<button onClick={submit}>OK</button>
</div>
)
}
これなら React のやり方でも困ることはなさそうに見えます
ただ vaildation とか フォーム内の summary タグみたいなものの開き閉じなど コンポーネントのメソッドを呼びたいようなところもあるんですよね
そういうのもすべて state として保持すべきというのが React のやり方なんでしょうけど そんなものまで親で保持するのはなぁと思うところもあります
あと App の state が更新されることになるので Form 内の input イベントで App が毎回レンダリングされます
Form 以外にコンポーネントがいくつかあるとそれらもレンダリングされ パフォーマンスに影響するかもしれません
それらのコンポーネントが Form のデータを使うなら再レンダリングされるべきですが そうでないなら無駄なので props が同じだったら再レンダリングしないように React.memo しておくほうがよいかもです