普通はそんなことをしないと思うのですが、Render Propsからコールバックを呼ぶような処理をすることとなり、一工夫が必要となりました。
TL; DR
- ReactはRender PhaseとCommit Phaseに分かれている
- Render PropsはRender Phaseだけど、
setState
はCommit Phaseから呼ばないといけない - 下層にコンポーネントをセットして、そこのイベントを介する
前提条件
もともとReact外を経由するような方法で値を取得して、onGet
コールバックを駆動するようなコンポーネントを作っていました。で、React 16.3で加わったContext
を使えばスッキリ書き直せるようにも思えてきたのですが、とりあえず従来のコンポーネントも(上に<Provider>
をセットすることを除けば)そのまま使えるようなものに置き換えておこうと考えました。
Render Propsとは
詳しくはReactのドキュメントや別の方の記事に譲りますが、Render Propsは、「prop
として、コンポーネントを返す関数を取る」技法です。この関数の引数を外側のコンポーネントでセットすることで、関数の出力となるコンポーネントの挙動を変化させることができます。
なお、prop
の名前がrender
とは限りません。実際、Context.Consumer
も子要素 =children
に関数を取ります。
素直に書くとエラーに
まずは、素直に書いてみました。
function ContextGetter(props){
return(
<Context.Consumer>
{
value => {
this.props.onGet(value);
return null;
}
}
</Context.Consumer>
);
}
このようにしたら動く…と思いきや、「render
の中からsetState
は呼べません」のようなエラーとなってしまいました。
2つのフェーズ
Reactの実行は、2つのフェーズに分かれています(ここの模式図が見やすいです)。
- Render Phase…描画を実行する部分。React 17で非同期化が計画されていることもあり、
setState
など副作用のある操作は行えない。 - Commit Phase…描画が完了した後処理の部分。
setState
などの実行も可能。
ということで、文字通りRender PhaseにあたるRender Propsでは、setState
を行うようなコールバックの実行はできないのでした。
では、どうする?
コールバックを呼ぶのはCommit Phaseでないとうまくいかないので、Consumer
の内側にさらにコンポーネントを入れて、内側のcomponentDidMount
やcomponentDidUpdate
などで拾うことにしました。
function ContextGetter(props){
return(
<Context.Consumer>
{
value => <ContextGetterInner value={value} onGet={this.props.onGet} />
}
</Context.Consumer>
);
}
class ContextGetterInner extends React.PureComponent{
componentDidMount(){
this.props.onGet(this.props.value);
}
componentDidUpdate(){
this.props.onGet(this.props.value);
}
}
結論
素直にContext
を使うほうが、ずっとわかりやすいや。