概要
React v16.3 で forwardRef API が追加されました。これを使った ref フォワーディングで、HOC を適用したコンポーネントの要素を取得することができます。しかしこの API が登場する前から、同様の機能は実装可能でした。この記事では、forwardRef API を使う書き方と使わない書き方を比較します。
課題
公式ドキュメントの Forwarding Refs のページに "This technique can also be particularly useful with higher-order components (also known as HOCs)" とあります。そこで、次のような課題を設定しました。
HOC を使う場面では、HOC の他に、3つのコンポーネントが登場します。HOC の定義は "Concretely, a higher-order component is a function that takes a component and returns a new component." とのこと。
- HOC: 共通の処理などを書く
- HOC の入力コンポーネント
- HOC の出力コンポーネント
- 「HOC の出力コンポーネント」を render するコンポーネント
この場面で、『「HOC の出力コンポーネント」を render するコンポーネント』で、「HOC の入力コンポーネント」の ref を取得するコードを検討します。
forwardRef API の基本
まずは、forwardRef API がどのようなものか、HOC ではないコンポーネントで見てゆきます。
具体例として、App コンポーネントが CustomTextInput という input タグをもつコンポーネントを render し、App でのその input タグの入力内容を取得するコードを考えます。
forwardRef API を使わない場合は、以下のようになります。
class App extends Component {
constructor(props){
super(props)
this.childRef = createRef()
}
log = () => console.log(this.childRef.current.value)
render() {
return (
<>
{/* JSX の特別な属性(props にならない)である ref ではなく、普通の属性(props) として inputRef を作成し、そこで ref を渡している */}
<CustomTextInput inputRef={this.childRef} />
<button onClick={this.log}>入力値</button>
</>
)
}
}
const CustomTextInput = props => {
return (
<>
CustomTextInput コンポーネントにある input タグ
{/* 呼び出し元の ref は、CustomTextInput の JSX の inputRef 属性として受け取っているので、それをセット */}
<input ref={props.inputRef} />
</>
)
}
forwardRef API を使う場合は、以下のようになります。
export default class App extends Component {
constructor(props){
super(props)
this.childRef = createRef()
}
log = () => console.log(this.childRef.current.value)
render(){
return (
<>
{/* ref 属性で渡す */}
<CustomTextInput ref={this.childRef} />
<button onClick={this.log}>入力値</button>
</>
)
}
}
const CustomTextInput = forwardRef((props, ref) => { // <= ここに ref がある
return (
<>
CustomTextInput コンポーネントにある input タグ
{/* 呼び出し元の ref を input の ref として設定している */}
<input ref={ref} />
</>
)
})
HOC と forwardRef API
forwardRef API の基本をおさえたところで、冒頭の課題に使ってゆきましょう。冒頭の課題とは、『「HOC の出力コンポーネント」を render するコンポーネント』で、「HOC の入力コンポーネント」の ref を取得する方法の検討です。
forwardRef は引数のコールバック関数の第二引数として、「HOC の出力コンポーネントを render するコンポーネント」から ref を受け取ることができます。それを「HOC を適用するコンポーネント」の属性として渡すとうまくゆきます。実装をみた方がわかりやすいです。
import React, { Component, createRef, forwardRef } from 'react'
// HOC
function withCustom(WrappedComponent) {
class Custom extends React.Component {
render() {
// ここで ref={XXX} としたくても、XXX 情報がない
return <WrappedComponent {...this.props} />
}
}
// return Custom
// そこで、`return Custom` ではなく、 以下のように forwardRef を適用する
return forwardRef((props, ref) => {
return <Custom {...props} forwardedRef={ref} />
})
}
// HOC を適用するコンポーネント
const TextInput = (props) => (
<input ref={props.forwardedRef}/>
)
// HOC を適用した
const CustomTextInput = withCustom(TextInput)
// HOC を render するコンポーネント
export default class App extends Component {
constructor(props){
super(props)
this.ref = createRef()
}
log = () => console.log(this.ref.current.value)
render(){
return (
<>
<CustomTextInput ref={this.ref}/>
<button onClick={this.log}>入力値</button>
</>
)
}
}
しかし、この課題では、forwardRef API を使わなくても以下のように実装できます。
import React, { Component, createRef, forwardRef } from 'react'
// HOC
function withCustom(WrappedComponent) {
class Custom extends React.Component {
componentDidMount() {
console.log('old props')
}
render() {
// this.props.inputRef を横流し
return <WrappedComponent {...this.props} />
}
}
return Custom
// return React.forwardRef((props, ref) => {
// return <Custom {...props} forwardedRef={ref} />
// })
}
// HOC を適用するコンポーネント
const TextInput = (props) => (
<input ref={props.inputRef}/>
)
const CustomTextInput = withCustom(TextInput)
// HOC を render するコンポーネント
export default class Parent extends Component {
constructor(props){
super(props)
this.ref = createRef()
}
log = () => alert(this.ref.current.value)
// ref= をやめて、inputRef= にした
render(){
return (
<>
<CustomTextInput inputRef={this.ref}/>
<button onClick={this.log}>入力値</button>
</>
)
}
}
所感
こんな感想を持ちました。
- 「これまで工夫で解決していた課題に対し、React.forwardRef という語彙を用意しました、便利でしょ」くらいの印象
- せっかくなのでこれからは使ってゆこう
- わざわざ既存実装を置き換えるほど重要ではなさそう