Reactで子コンポーネントに関数を受け渡す方法は以下のようにpropsで橋渡しをすることがわかりました。
ですが、今回のように子コンポーネントの中に引数が存在して、それを処理したい場合どうするかというのが課題で、調べてみた結果、親コンポーネントから子コンポーネントへ値を受け渡すのは可能なのに対し、ReactにはVueやAngularのように子コンポーネントから親コンポーネントへ値を受け渡すemitメソッドのようなものは存在しないようです。
ところが、今回実装したいのはイベントが子コンポーネントのJSXにあり、しかも親コンポーネントから引っ張ってきた値を子コンポーネントで処理して、親コンポーネントにある結果欄に値を出力する操作です。…その方法を探ってみた結果、Reactでの解釈は
- 子コンポーネントで行われるべきイベント処理を受け渡し、親コンポーネントで同期する
これで、次のようなデータ処理(サンプルとして電卓機能を作成)が可能になります。
※次章では、子コンポーネントしか持っていない値を親コンポーネントに受け渡す方法も載せています。
1:イベントのみが子コンポーネントに存在する場合
値を表示させる
元になったサイトは以下の通りで、コンポーネントを使って簡易な電卓ツールを作成していきます。
これを応用して電卓のキーを作成します。
では、このキーを打鍵すると文字が表示されるようにして、以下のように振り分けます。
- 文字が数字の場合は、数値に変換する。引き続き数字の場合は桁をずらし、計算記号が打たれた場合は被序数を決定。
- 文字が計算記号の場合は、被序数と序数を確定し、計算を行う
- 文字が=の場合は合計を計算する。
- 文字がCの場合は全てリセットする。
このようにして作ったサンプルが以下のプログラムとなります。
import React from "react";
import Setkey from "./Setkey"
export default class Calc extends React.Component {
constructor(props){
super(props)
this.state = {
lnum: null, //今までの値
cnum: 0, //追加の値
sum: 0, //合計値
str: '', //打ち込んだ文字
sign: '', //計算記号
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','+']],
],
}
this.getCharstate = this.getChar.bind(this) //メソッドのバインド
}
getChar(chr,str){
let lnum = this.state.lnum //被序数
let cnum = this.state.cnum //序数
let sum = this.state.sum //合計
let sign = this.state.sign
str = this.state.str + str
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 = this.calc(sign,lnum,cnum)
}else{
if(chr == "sub"){
lnum = 0
}
lnum = cnum
}
sign = chr
cnum = 0
}else if( chr == "eq"){
lnum = this.calc(sign,lnum,cnum)
sum = lnum
}else{
lnum = null
cnum = 0
sum = 0
str = ''
}
this.setState({lnum: lnum})
this.setState({cnum: cnum})
this.setState({sum: sum})
this.setState({str: str})
this.setState({sign: sign})
}
//計算処理
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
}
render(){
return (
<>
{
this.state.vals.map((val,key)=>{
return (
<div key={key}>
{
val.map((v,i)=>{
return(<Setkey key={i} value={v[1]} getChar={()=>{this.getChar(v[0],v[1])} }/>)
})
}
</div>
)
})
}
<div>
<p>打ち込んだ文字:{this.state.str}</p>
<p>合計:{this.state.sum}</p>
</div>
</>
)
}
}
import React from "react";
export default class Setkey extends React.Component {
render() {
return (
<button className="square" onClick={()=>{this.props.getChar()}}>
{this.props.value}
</button>
);
}
}
これで最低限の電卓として機能していますが、ここからが本題です。クリックイベントとそれによるgetCharメソッドは子コンポーネントで生成されたJSX内に存在しているのですが、だからといって前述したように子コンポーネントに処理用メソッドを作って計算したとしても、親コンポーネントに値を受け渡せません。そこで、親コンポーネントに子コンポーネントと同じ関数を橋渡ししておき、子コンポーネントからイベントが実行されても、親コンポーネントがそのイベントを受けたように見せかけ、それを同期させることで実現できます。
なので、まずはテンプレートの生成部分をこのように記述する必要があります。
<Setkey key={i} value={v[1]} getChar={()=>{this.getChar(v[0],v[1])} }/>
ここで注意しなければいけないのは、厳密には式左側のgetCharとメソッドのthis.getCharは別物で、あくまで式の左側は、子コンポーネントに橋渡ししている、同期用のメソッド(コールバック関数)になります。
なので、書き分けてみるとこのようになり、海外サイトではhogeStateをコールバック関数に、hogeをメソッドとしていることが多く見られます。
return(<Setkey key={i} value={v[1]} getCharstate={()=>{this.getChar(v[0],v[1])} }/>)
export default class Setkey extends React.Component {
render() {
return (
<button className="square" onClick={()=>{this.props.getCharState()}}>
{this.props.value}
</button>
);
}
}
2:子コンポーネントからイベントと値を受け渡す
子コンポーネントからイベントと値を受け渡す場合は、先程使用しなかった子コンポーネント内のコールバック関数に引数を代入し、それを親コンポーネント内、無名関数の引数に設定、その値をそのままメソッドの引数に代入することで実現できます。
先程の電卓のキー配置部分も子コンポーネントに移動し、キーの値も子コンポーネントから受け渡すように改造してみました。
import React from "react";
import Setkey from "./Setkey2"
export default class Calc extends React.Component {
constructor(){
super()
this.state = {
lnum: null, //今までの値
cnum: 0, //追加の値
sum: 0, //合計値
str: '', //打ち込んだ文字
sign: '', //計算記号
}
this.getCharstate = this.getChar.bind(this) //メソッドのバインド
}
getChar(chr,str){
let lnum = this.state.lnum //被序数
let cnum = this.state.cnum //序数
let sum = this.state.sum //合計
let sign = this.state.sign
str = this.state.str + str
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 = this.calc(sign,lnum,cnum)
}else{
if(chr == "sub"){
lnum = 0
}
lnum = cnum
}
sign = chr
cnum = 0
}else if( chr == "eq"){
lnum = this.calc(sign,lnum,cnum)
sum = lnum
}else{
lnum = null
cnum = 0
sum = 0
str = ''
}
this.setState({lnum: lnum})
this.setState({cnum: cnum})
this.setState({sum: sum})
this.setState({str: str})
this.setState({sign: sign})
}
//計算処理
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
}
render(){
return (
<>
{
<Setkey
getCharstate = { (c,s)=>this.getChar(c,s)}
/>
}
<div>
<p>打ち込んだ文字:{this.state.str}</p>
<p>合計:{this.state.sum}</p>
これまでの値{this.state.lnum}<br/>
追加の値{this.state.cnum}<br/>
打ち込んだ文字{this.state.str}<br/>
</div>
</>
)
}
}
import React from "react";
export default class Setkey extends React.Component {
constructor(props){
super(props)
this.state = {
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','+']],
]
};
}
render() {
return (
<>
{this.state.vals.map((val,key)=>{
return (
<div key={key}>
{
val.map((v,i)=>{
return(
<React.Fragment key={i}>
<button className="square" onClick={()=>{this.props.getCharstate(v[0],v[1])}}>
{v[1]}
</button>
</React.Fragment>
)
})
}
</div>
)
})
}
</>
)
}
}
ポイントは以下の部分です。この引数cとsは子コンポーネントのイベントの引数に入っていたもので、それを直接プッシュキーを操作するgetCharメソッドの引数に渡しています。
<Setkey
getCharstate = { (c,s)=>this.getChar(c,s)}
/>
3:子コンポーネントから値のみを受け渡す
子コンポーネントからロード時に値を受け渡すにはライフサイクルメソッドの一つ、componentDidMountメソッドを利用します。先程のシステムからタイトルを子コンポーネントから取得するように改造してみました。
注意点は親コンポーネントにも受け皿となる変数を定義しておく必要があります。また、何も処理を施さず親コンポーネントで値を表示させるだけなら、コールバック関数をsetStateメソッドで受け取ることもできます。
import React from "react";
import Setkey from "./Setkey3"
export default class Calc extends React.Component {
constructor(){
super()
this.state = {
lnum: null, //今までの値
cnum: 0, //追加の値
sum: 0, //合計値
str: '', //打ち込んだ文字
sign: '', //計算記号
title: '', //タイトル格納用
}
this.getCharstate = this.getChar.bind(this) //メソッドのバインド
}
/*中略*/
render(){
return (
<>
<h4>{this.state.title}</h4>
{
<Setkey
setTitle = {(t)=>this.setState({title:t})}
getCharstate = { (c,s)=>this.getChar(c,s)}
/>
}
<div>
<p>打ち込んだ文字:{this.state.str}</p>
<p>合計:{this.state.sum}</p>
</div>
</>
)
}
}
import React from "react";
export default class Setkey extends React.Component {
constructor(props){
super(props)
this.state = {
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','+']],
]
};
}
//このメソッドがライフサイクルメソッドで、ロード時に実行される
componentDidMount(){
const title = "クラスコンポーネントで電卓演習"
this.props.setTitleState(title) //同期用メソッド
}
render() {
return (
<>
{this.state.vals.map((val,key)=>{
return (
<div key={key}>
{
val.map((v,i)=>{
return(
<React.Fragment key={i}>
<button className="square" onClick={()=>{this.props.getCharstate(v[0],v[1])}}>
{v[1]}
</button>
</React.Fragment>
)
})
}
</div>
)
})
}
</>
)
}
}
関数コンポーネントの場合
関数コンポーネントの場合は、新しい記事で独立させました。