Reactで子コンポーネントに関数を受け渡すにはpropsを用います。
ですが、今回のように子コンポーネントの中に引数が存在して、それを親コンポーネントに受け渡したい場合どうするかというのが課題で、親コンポーネントから子コンポーネントへ値を受け渡すのは可能なのに対し、ReactにはVueやAngularのように子コンポーネントから親コンポーネントへ値を受け渡すemitメソッドのようなものは存在しないようです。
しかも、かつてReactの技術系サイトでは、子コンポーネントから親コンポーネントへの値の受け渡しは仕様上不可能では?あるいは不要では?と説明していた部分が多く見受けられました。ですが、本当に子コンポーネントに設定された値を呼び出すのが不可能なのか海外サイトを中心に閲覧を続けてみたところ、以下のコールバック関数を用いる方法で実現可能だとわかり、動作検証をしてみましたのでそれをまとめておきます。
※今日ではReact16.8以降のフックを用いた関数コンポーネントでの記述が主流なので、改訂版記事ではクラスコンポーネントの解説は割愛させていただきます(旧記事をクラスコンポーネント専用に書き換えています)。
1:イベントのみを子コンポーネントから受け渡す場合
イベントメソッドのみが子コンポーネントに存在する場合は、コールバック関数を用いれば大丈夫です。
イベント処理を受け渡す
元になったサイトは以下の通りで、コンポーネントを使って簡易な電卓ツールを作成していきます。
これを応用して電卓のキーを作成します。
では、このキーを打鍵すると文字が表示されるようにして、以下のように振り分けます。
- 文字が数字の場合は、数値に変換する。引き続き数字の場合は桁をずらし、計算記号が打たれた場合は被序数を決定。
- 文字が計算記号の場合は、被序数と序数を確定し、計算を行う
- 文字が=の場合は合計を計算する。
- 文字がCの場合は全てリセットする。
このようにして作ったサンプルが以下のプログラムとなります。
- 親コンポーネント
import React, { useState, useEffect } from "react";
import Setkey from "./Setkey";
const Calc = ()=>{
const[ d_lnum, setLnum ] = useState(null)
const[ d_cnum, setCnum ] = useState(0)
const[ d_sum, setSum ] = useState(0)
const[ d_str, setStr ] = useState("")
const[ d_sign, setSign ] = useState("")
const vals = [
[['7','7'],['8','8'],['9','9'],['div','÷']],
[['4','4'],['5','5'],['6','6'],['mul','×']],
[['1','1'],['2','2'],['3','3'],['sub','-']],
[['0','0'],['eq','='],['c','C'],['add','+']],
]
const getChar = (chr,strtmp)=>{
let lnum = d_lnum //被序数
let cnum = d_cnum //序数
let sum = d_sum //合計
let sign = d_sign
let str = d_str + strtmp
//console.log(lnum,cnum,sum,str,sign)
if(chr.match(/[0-9]/g)!== null){
let num = parseInt(chr)
cnum = cnum * 10 + num //数値が打ち込まれるごとに桁をずらしていく
}else if(chr.match(/(c|eq)/g) == null){
if(lnum != null){
lnum = calc(sign,lnum,cnum)
}else{
if(chr == "sub"){
lnum = 0
}
lnum = cnum
}
sign = chr
cnum = 0
}else if( chr == "eq"){
lnum = calc(sign,lnum,cnum)
sum = lnum
}else{
lnum = 0
cnum = 0
sum = 0
str = ''
}
setLnum(lnum)
setCnum(cnum)
setSum(sum)
setStr(str)
setSign(sign)
}
//計算処理
const calc = (mode,lnum,cnum)=>{
switch(mode){
case "add": lnum = cnum + lnum
break;
case "sub": lnum = lnum - cnum
break;
case "mul": lnum = lnum * cnum
break;
case "div": lnum = lnum / cnum
break;
}
return lnum
}
return (
<>
{
vals.map((val,key)=>{
return (
<div key={key}>
{
val.map((v,i)=>{
return(<Setkey key={i} val={v[1]} getChar={()=>{getChar(v[0],v[1])} }/>)
})
}
</div>
)
})
}
<div>
<p>打ち込んだ文字:{d_str}</p>
<p>合計:{d_sum}</p>
</div>
</>
)
}
export default Calc
- 子コンポーネント
import React from "react";
const Setkey = (props)=>{
const { getChar,val } = props
return (
<button className="square" onClick={()=>{getChar()}}>
{val}
</button>
)
}
export default Setkey
これで最低限の電卓として機能していますが、ここから本題に入っていきます。クリックイベントとそれによるgetCharメソッドは子コンポーネントで生成されたJSX内に存在しているのですが、子コンポーネントに値は用意されていません。
ですが、コールバック関数を用いてイベント処理を受け渡しできるので、getCharメソッドが発火されたタイミングで親コンポーネントの同期用メソッドgetCharに配置された引数v[0]とv[1]が引数を使ってgetCharメソッドによる計算処理が行われます。
return(<PushKey val={v[1]} getChar={()=>{getChar(v[0],v[1])} }/>)
ここで注意しなければいけないのは、親コンポーネントの式左側と式右側のgetCharは別物で、式左側は同期用のコールバック関数になります。
なので、敢えて書き分けてみるとこのようになり、今日、海外サイトでは分別しやすいようにメソッド部分をhoge、コールバック関数部分をhogeStateとすることが多いようです。
{/*左側はコールバック関数、右側は処理用メソッド*/}
return(<PushKey val={v[1]} getCharState={()=>{getChar(v[0],v[1])} }/>)
{/*子コンポーネントに置かれたコールバック関数*/}
<button className="square" onClick={()=>{getCharState()}}>
2:イベントと値を子コンポーネントから受け渡す場合
1章を踏まえ、本題の子コンポーネントの値を親コンポーネントに受け渡す処理の説明に入ります。まずは、イベント発火によって値を受け渡す場合で、これを用いることで任意のキーやインデックスを受け渡すことができます。
今度はTypescriptを用いたtsxファイルの説明となりますが、基本的な部分は変わっていません。
子コンポーネントの値を親コンポーネントに呼び出す
例はTypescriptを使用した簡潔なToDoシステム(ソース元はVue3)をReactに改造してステータスを切り替えられるようにしたプログラムですが、ステータスを変更する際に子コンポーネントに設定されたインデックスを呼び出しています。
import React,{ useState } from 'react';
import Todo from './components/Todo'
import AddTodo from './components/AddTodo'
import './Styles.css'
//可読性を上げるためにmodelの導入
import { ToDo } from './todo.model'
const App: React.VFC =()=>{
const [todos, setTodos] = useState<ToDo[]>([]);
const [title,setValue] = useState(null);
const todoAdd = (text: string,state: string)=>{
setTodos(prevTodos => [
...prevTodos,
{id: Math.random().toString(), text: text,state: "未作業"}
])
}
const toggle = (i: number,slist: any)=>{
const stat = {...slist[i]}
//ステータス切り替え
stat.state = (stat.state === '未作業') ? '作業中' : '作業完了';
slist[i] = stat
setTodos([...slist])
}
const deleteList = (i: number)=>{
setTodos(todos.filter((_,idx)=> idx !== i))
}
return (
<div className="nav">
<h1>React-typescript-TODO テスト React18</h1>
{ todos.length }件を表示中
<table>
<thead>
<tr>
<th className="id">ID</th>
<th className="comment">コメント</th>
<th className="state">状態</th>
<th className="button">-</th>
</tr>
</thead>
<tbody>
<Todo
items={todos}
toggleState={(index:any)=>toggle(index,todos)}
delState={(index:any)=>deleteList(index)}
/>
</tbody>
</table>
<h2>新しい作業の追加</h2>
<AddTodo todoAdded={todoAdd} />{/*フォーム制御*/}
</div>
);
}
export default App;
//オブジェクトのインターフェース設定
interface TodoProps{
items: {
id: string,
text: string,
state: any
}[],
toggleState: any,
}
const Todo: React.FC<TodoProps> = props =>{
return(
<>
{props.items.map((list,idx) =>(
<tr key={idx}>
<td>{list.id}</td>
<td>{list.text}</td>
<td className="state">
<button onClick={()=>props.toggleState(idx)}>
{list.state}
</button>
</td>
<td className="button">
<button onClick={()=>props.delState(idx)}>
削除
</button>
</td>
</tr>
))}
</>
)
}
export default Todo;
コールバック関数を使ってインライン関数の引数から値を渡す
本題は以下の部分です。toggleStateメソッドの引数はmapメソッドによって渡された配列のインデックスになり、そのインデックスをTodo.tsxファイル内にあるコールバック関数toggleState内に記述されたインライン関数の引数に用意します。そして、インライン関数内のメソッドtoggleの引数に代入してメソッド内で処理し、同期を取る流れとなります。
<>
{props.items.map((list,idx) =>(
<tr key={idx}>
<td>{list.id}</td>
<td>{list.text}</td>
<td className="state">
{/*引数idxは選択したリストのインデックスが格納される*/}
<button onClick={()=>props.toggleState(idx)}>
{list.state}
</button>
</td>
<td className="button">
<button>
削除
</button>
</td>
</tr>
))}
</>
const toggle = (i: number,slist: any)=>{
const stat = {...slist[i]} //修正したいリストを抽出
stat.state = (stat.state === '未作業') ? '作業中' : '作業完了'; //ステータス切り替え
slist[i] = stat //代入
setTodos([...slist]) //フックで反映、分割代入にしないと反映されない。
}
{/*中略*/}
{/*引数idxが子コンポーネントから呼び出した対象リストのインデックス*/}
<Todo items={todos} toggleState={(idx:any)=>toggle(idx,todos)} />
このようにすれば、子コンポーネントが持っている値を親コンポーネントに受け渡し、同期をとることができます。削除の場合はインデックスだけ取得すれば大丈夫です。
※選択したリストの状態を変更するために配列Todosのインデックスを子コンポーネントから呼び出しています。
3:値だけを子コンポーネントから受け渡す場合
イベント発火なしで子コンポーネントの値だけを親コンポーネントに値を引き渡したい場合、子コンポーネントでuseEffectフックを用いれば実現可能です。ここのフック内に受け渡し用のコールバック関数を作っておき、今までと同様に親コンポーネントに値と引数を渡します。そして親コンポーネントでuseStateフックを使用して同期を取ります(インライン関数でダイレクトに同期用メソッドsetHogeを使用すると効率がいいです)。
先程のTodoツールのタイトルを子コンポーネントから受け渡すように改造してみました。
import React,{ useState } from 'react';
import Todo from './components/Todo'
import AddTodo from './components/AddTodo'
import './Styles.css'
//可読性を上げるためにmodelの導入
import { ToDo } from './todo.model'
const App: React.VFC =()=>{
{/*中略*/}
const [title,setTitle] = useState(null);
{/*中略*/}
return (
<div className="nav">
{/*子コンポーネントに設定したタイトルが表示される*/}
<h1>{title}</h1>
{/*中略*/}
{/*setTitleStateの*/}
<Todo
items={todos}
toggleState={(index:any)=>toggle(index,todos)}
delState={(index:any)=>deleteList(index)}
setTitleState={(val:any)=>setTitle(val)}
/>
);
}
export default App;
import React,{ useEffect } from 'react';
//オブジェクトのインターフェース設定
interface TodoProps{
items: {
id: string,
text: string,
state: any
}[],
toggleState: any,
setTitleState:any, //コールバック関数を設定しておく
delState:any,
}
const Todo: React.FC<TodoProps> = props =>{
useEffect(()=>{
const title = 'React-typescript-TODO テスト React18' //親コンポーネントに反映させるタイトル
props.setTitleState(title) //受け渡し用のコールバック関数
},[props])
return(
<>
{props.items.map((list,idx) =>(
<tr key={idx} >
<td>{list.id}</td>
<td>{list.text}</td>
<td className="state">
<button onClick={()=>props.toggleState(idx)}>
{list.state}
</button>
</td>
<td className="button">
<button onClick={()=>props.delState(idx)}>
削除
</button>
</td>
</tr>
))}
</>
)
}
export default Todo;
4:子コンポーネントから親コンポーネントにイベントと値を受け渡し、再度子コンポーネントに反映させる場合
わかりづらいですが、子→親→子となる場合です。子コンポーネントには同じ変数が用意されています。
親は子に対し、値を受け渡さなければいけないので、datastate={data}と受け渡しの準備をしておきます。
import React,{useState} from 'react';
import Test2 from './Test2';
const Test = ()=>{
const [data,setData] = useState(null)
function bind(idx){
const add = idx * 2
setData(add)
}
return(
<>
<Test2 fromChildState={(idx)=>(bind(idx))} datastate={data} />
</>
)
}
export default Test
対する子コンポーネントは受け渡されたprops.datastateに対し、同期をとる必要があるので、それでこちら側もsetData2とuseStateで制御しておきます。そしてuseEffectフックを使って、props.datastateがnullでなくなった場合のみ、同期を取るようにすれば、子コンポーネントに対し、初期値も親コンポーネントを経由し同期処理した値も表示されるようになります(親のsetDataをpropsに持っていく方法もあります)。
import React,{useState,useRef,useEffect} from 'react'
const Test2 = (props)=>{
const {datastate,fromChildState} = props
const ini = 2
const [data2,setData2 ] = useState(ini)
useEffect(()=>{
if(props.datastate != null){
setData2(props.datastate)
}
},[props])
return(
<>
<button onClick={()=>fromChildState(data2)}>てすと</button>
{ data2 }
</>
)
}
export default Test2
クラスコンポーネントの場合
旧記事をクラスコンポーネント専用のまとめサイトにしましたので、そちらをご参照ください。