◆ WebComponent 風にメソッド呼び出ししたい
   ◆ 子コンポーネントで状態を保持してそれを取得
   ◆ またはフォームのリセットや自動計算やバリデーションなどの何かの処理を行う命令
◆ ref や hook でできなくもない
◆ 工夫すれば React 推奨の方法でも近い形で書けた

最近 React でいくつかページを作ることがあって 意外な不便なところだったり これどうすればいいのと思うところがあったので それらをまとめてたのですが 長くなったのでいくつかの記事に分けます
数記事 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 しておくほうがよいかもです