子コンポーネントは状態を管理させないで、 親コンポーネントですべて状態を管理する方法が基本だとは思う。
しかし ここでは、子コンポーネントで状態を管理して、 親コンポーネントからそれを任意のタイミングで把握する方法を調べます。
使用した環境:
$ node --version
v22.17.0
$ npm -version
10.9.2
hello-use-ref プロジェクトを作成:
npm create vite hello-use-ref --template react-ts
質問されるので、React + TypeScript を選択した。
プロジェクトルートへ移動:
cd hello-use-ref
依存するモジュールを入れる:
npm install
ポートを 8000 で起動するように修正:
package.json:
"scripts": {
"dev": "vite --port 8000",
--port 8000 を追加する。
起動して確かめる:
npm run dev
ブラウザで http://localhost:8000/ にアクセスします。
src/App.tsx を次のように書きかえる:
import React from 'react'
const MyButton = (props: {
label: string
onClick: ()=>void
}): React.ReactElement => {
return (
<button
type="button"
onClick={()=>props.onClick()}
>{props.label}</button>
)
}
const App = (): React.ReactElement => {
const onSaveButtonClick = ():void =>{
console.log("Save Button is Clicked")
}
return (
<>
<MyButton
label="Save"
onClick={onSaveButtonClick}
/>
</>
)
}
export default App
起動して作動を確かめる:
$ npm run dev
Yes / No の2つの状態を持つ子コンポーネントを作成します。
事前準備として MyRadioButton を用意:
const MyRadioButton = (props: {
id: string
name: string
label: string
checked: boolean
onChange: ()=>void
}): React.ReactElement => {
return (
<>
<input
type="radio"
name={props.name}
id={props.id}
checked={props.checked}
onChange={()=>props.onChange()}
/>
<label
htmlFor={props.id}
>{props.label}</label>
</>
)
}
本体の子コンポーネントとなる YesNoPanel を用意:
type YesNo = 'Yes' | 'No'
const YesNoPanel = (): React.ReactElement => {
const [yesNoValue, setYesNoValue] = useState<YesNo>('Yes')
const onYesRadioButtonChange = ():void => { setYesNoValue('Yes') }
const onNoRadioButtonChange = ():void => { setYesNoValue('No') }
return (
<div>
<MyRadioButton
id="YesOption"
name="YesNoRadioButtons"
label={"Yes"}
checked={(yesNoValue==='Yes')}
onChange={onYesRadioButtonChange}
/>
<MyRadioButton
id="NoOption"
name="YesNoRadioButtons"
label={"No"}
checked={(yesNoValue==='No')}
onChange={onNoRadioButtonChange}
/>
</div>
)
}
子コンポーネントから値を受け取るための type YesNoCallbackFn を定義:
type YesNoCallbackFn = () => YesNo;
App を修正:
const App = (): React.ReactElement => {
const yesNoCallbackFnRef = useRef<YesNoCallbackFn | undefined>(undefined);
const onSaveButtonClick = ():void =>{
const fn: YesNoCallbackFn | undefined = yesNoCallbackFnRef.current
if (fn!==undefined) {
const yesOrNo: YesNo = fn()
console.log(`@ ${yesOrNo}`)
}
}
...
return (
<>
<YesNoPanel
yesNoCallbackFnRef={yesNoCallbackFnRef}
/>
...
useRef を使って yesNoCallbackFnRef を用意。 これを YesNoPanel に渡すのと同時に onSaveButtonClick 時には yesNoCallbackFnRef.current を使って YesNoPanel の現在の状態(Yes か No か)を取得しています。 結果を確認するために console.log しています。
YesNoPanel を修正:
const YesNoPanel = (props: {
yesNoCallbackFnRef: ReturnType<typeof useRef<YesNoCallbackFn | undefined>>
}): React.ReactElement => {
const [yesNoValue, setYesNoValue] = useState<YesNo>('Yes')
useEffect(()=>{
const fn: YesNoCallbackFn = ():YesNo =>{ return yesNoValue }
props.yesNoCallbackFnRef.current = fn
}, [yesNoValue])
yesNoCallbackFnRef の型は ReturnType<typeof useRef<YesNoCallbackFn | undefined>> だとのこと。(claude に聞いた。)
yesNoValue が変わるたびに useEffect でコールバックする関数を更新する。 こうすることで、親コンポーネントから useRef で定義した yesNoCallbackFnRef.current を適用すること(コールするといえばいいのか)で 子コンポーネントの状態が Yes だったか No だったかを受け取ることができる。
その部分は別に関数ではなく、単に YesNo タイプの値でよい気はする。 関数にすることでうれしいことあるのかな?(わからない) とりあえずこの例では、関数を使うメリットはないのでその点を修正したコードを最後に追記する。
完成した App.tsx:
import React, { useState, useRef, useEffect } from 'react'
const MyButton = (props: {
label: string
onClick: ()=>void
}): React.ReactElement => {
return (
<button
type="button"
onClick={()=>props.onClick()}
>{props.label}</button>
)
}
const MyRadioButton = (props: {
id: string
name: string
label: string
checked: boolean
onChange: ()=>void
}): React.ReactElement => {
return (
<>
<input
type="radio"
name={props.name}
id={props.id}
checked={props.checked}
onChange={()=>props.onChange()}
/>
<label
htmlFor={props.id}
>{props.label}</label>
</>
)
}
type YesNo = 'Yes' | 'No'
type YesNoCallbackFn = () => YesNo;
const YesNoPanel = (props: {
yesNoCallbackFnRef: ReturnType<typeof useRef<YesNoCallbackFn | undefined>>
}): React.ReactElement => {
const [yesNoValue, setYesNoValue] = useState<YesNo>('Yes')
useEffect(()=>{
const fn: YesNoCallbackFn = ():YesNo =>{ return yesNoValue }
props.yesNoCallbackFnRef.current = fn
}, [yesNoValue])
const onYesRadioButtonChange = ():void => { setYesNoValue('Yes') }
const onNoRadioButtonChange = ():void => { setYesNoValue('No') }
return (
<div>
<MyRadioButton
id="YesOption"
name="YesNoRadioButtons"
label={"Yes"}
checked={(yesNoValue==='Yes')}
onChange={onYesRadioButtonChange}
/>
<MyRadioButton
id="NoOption"
name="YesNoRadioButtons"
label={"No"}
checked={(yesNoValue==='No')}
onChange={onNoRadioButtonChange}
/>
</div>
)
}
const App = (): React.ReactElement => {
const yesNoCallbackFnRef = useRef<YesNoCallbackFn | undefined>(undefined);
const onSaveButtonClick = ():void =>{
const fn: YesNoCallbackFn | undefined = yesNoCallbackFnRef.current
if (fn!==undefined) {
const yesOrNo: YesNo = fn()
console.log(`@ ${yesOrNo}`)
}
}
return (
<>
<YesNoPanel
yesNoCallbackFnRef={yesNoCallbackFnRef}
/>
<MyButton
label="Save"
onClick={onSaveButtonClick}
/>
</>
)
}
export default App
YesNoPanel の状態を知るためにはわざわざ関数を使う必要もないので、単に YesNo の値を current にセットするように変更した改善版 App.tsx:
import React, { useState, useRef, useEffect } from 'react'
const MyButton = (props: {
label: string
onClick: ()=>void
}): React.ReactElement => {
return (
<button
type="button"
onClick={()=>props.onClick()}
>{props.label}</button>
)
}
const MyRadioButton = (props: {
id: string
name: string
label: string
checked: boolean
onChange: ()=>void
}): React.ReactElement => {
return (
<>
<input
type="radio"
name={props.name}
id={props.id}
checked={props.checked}
onChange={()=>props.onChange()}
/>
<label
htmlFor={props.id}
>{props.label}</label>
</>
)
}
type YesNo = 'Yes' | 'No'
const YesNoPanel = (props: {
yesNoRef: ReturnType<typeof useRef<YesNo | undefined>>
}): React.ReactElement => {
const [yesNoValue, setYesNoValue] = useState<YesNo>('Yes')
useEffect(()=>{
props.yesNoRef.current = yesNoValue
}, [yesNoValue])
const onYesRadioButtonChange = ():void => { setYesNoValue('Yes') }
const onNoRadioButtonChange = ():void => { setYesNoValue('No') }
return (
<div>
<MyRadioButton
id="YesOption"
name="YesNoRadioButtons"
label={"Yes"}
checked={(yesNoValue==='Yes')}
onChange={onYesRadioButtonChange}
/>
<MyRadioButton
id="NoOption"
name="YesNoRadioButtons"
label={"No"}
checked={(yesNoValue==='No')}
onChange={onNoRadioButtonChange}
/>
</div>
)
}
const App = (): React.ReactElement => {
const yesNoRef = useRef<YesNo | undefined>(undefined);
const onSaveButtonClick = ():void =>{
const yesOrNo: YesNo | undefined = yesNoRef.current
if (yesOrNo!==undefined) {
console.log(`@ ${yesOrNo}`)
}
}
return (
<>
<YesNoPanel
yesNoRef={yesNoRef}
/>
<MyButton
label="Save"
onClick={onSaveButtonClick}
/>
</>
)
}
export default App
以上です。