React におけるRef
には、2つの意味がある。
ひとつはRef
オブジェクト。もうひとつは DOM 要素やコンポーネントのref
属性。
この2つは関連がないわけではないが、それぞれ別の概念なので、分けて考えたほうが理解しやすい。
まずRef
オブジェクトについて扱い、次にref
属性について見ていく。
なお、この記事のコードは React のv16.10.2
で動作確認している。
Ref オブジェクト
最初にRef
オブジェクトの特徴を述べると、コンポーネントがマウントされたときからアンマウントされるときまで存在し続ける、書き換え可能なオブジェクト、である。
この特徴を理解するためにまず、Ref
ではない一般的なオブジェクトの挙動を確認する。
以下のApp
コンポーネントは、ボタンを押下するとstate
が更新され、その度に再レンダー、つまりApp
という関数が再呼び出しされる。
その際、X
とコメントした部分のログは何を表示するだろうか。
import React, {useState} from 'react'; const list = []; const App = () => { const [count, setCount] = useState(0); const onClick = () => { setCount(c => c + 1); }; const obj = {}; // objは、`Ref`ではない一般的なオブジェクト list.push(obj); if (list.length >= 2) { console.log(list[list.length - 2] === list[list.length - 1]); // X } return ( <div> <button type="button" onClick={onClick}> count up </button> {count} </div> ); }; export default App;
答えは、常にfalse
を表示する、である。
これは、App
関数が実行される度に新しいobj
が生成されていることを意味する。
count
が0
だったときのobj
とcount
が1
だったときのobj
は、値は同じ(どちらも空)だが、それぞれ別のオブジェクトである。
Ref
オブジェクトは、これとは別の振る舞いをする。
Ref
オブジェクトを生成するにはuseRef
を使うので、先程のコードを次のように書き換える。
- import React, {useState} from 'react'; + import React, {useState, useRef} from 'react'; - const obj = {}; + const obj = useRef();
すると、X
は常にtrue
となり、obj
は同一のオブジェクトを指すようになる。
つまり、マウント時にuseRef
で生成したRef
オブジェクトを、そのまま使い続けている。このRef
オブジェクトは、コンポーネントがアンマウントされるまで存在し続ける。
Ref
オブジェクトは最初からcurrent
というプロパティを持っており、useRef
に渡した引数がcurrent
プロパティの初期値となる。引数を渡さなかった場合はundefined
が初期値になる。
そしてcurrent
は、自由に書き換えることが出来る。
最初に述べたように、アンマウントされるまで存在し続けること、自由に書き換えることが出来ること、これがRef
オブジェクトの特徴である。
このことは例えば、関数コンポーネントを再レンダーした際に、前回のレンダー時のデータを取得することが可能ということを意味する。
例として、前回のレンダー時のprops
を取得するコンポーネントを書いた。
Child
はApp
からprops
としてcount
を受け取り、それをNow
として表示する。それとは別に、前回のcount
の値をref.current
に保存しておき、それをBefore
として表示する。
import React, {useState, useRef, useEffect} from 'react'; const App = () => { const [count, setCount] = useState(0); const onClick = () => { setCount(c => c + 1); }; return ( <div> <Child count={count} /> <button type="button" onClick={onClick}> count up </button> </div> ); }; const Child = ({count}) => { const ref = useRef('--'); useEffect(() => { ref.current = count; }); return ( <div> Before: {ref.current} <br /> Now: {count} </div> ); }; export default App;
マウント時にChild
に渡されるprops.count
は0
。そしてref.current
の初期値は--
。
その内容に基づいて DOM の更新が行われたあと、useEffect
が実行されてref.current
に0
が格納される。
App
のボタンを押すとChild
のprops.count
が1
に更新されるので、再レンダーが発生する。
このとき、ref.current
には0
が入っているので、Before
としてそれが表示される。
そして DOM の更新が終わったあと再びuseEffect
が実行され、ref.current
に今度は1
が格納される。
ボタンを押す度にこれが繰り返される。
Ref
オブジェクトはprops
やstate
とは違い、レンダー毎に値が作られるのではないので、レンダーと値が一対一になっていない。そして値が自由に書き換え可能であるため、同じレンダーのなかでも値が変わり得る。
これは、「宣言的に UI を記述する」という React の流儀に反する。便利に使えるケースもあるが多用はせず、React の原則から逸脱せざるを得ないときに、原則から逸脱していることを自覚しながら使うべき。
Ref
オブジェクトのcurrent
プロパティは、その値が更新されても React によってそのことが通知されないので、その点にも注意する。
別の言い方をすると、current
プロパティの更新をトリガーにして自動的に何かが発生することはない。
これも、state
と比較すると分かりやすい。
state
の場合、値が更新されれば React はその内容に応じて再レンダーを行う。
そのため、下記の例では、ボタンを押した1
秒後に表示が更新される。
import React, {useState} from 'react'; const App = () => { const [count, setCount] = useState(0); const onClick = () => { setTimeout(() => { setCount(c => c + 1); }, 1000); }; return ( <div> {count} <br /> <button type="button" onClick={onClick}> count up </button> </div> ); }; export default App;
だがRef
オブジェクトのcurrent
プロパティの場合、値が更新されても、それだけでは何も発生しない。
下記の例の場合、ボタンを押した1
秒後にref.current
の値がインクリメントされるが、再レンダーは発生しないため、ただ値が変わるだけであり表示にはそれが反映されない。
import React, {useRef} from 'react'; const App = () => { const ref = useRef(0); const onClick = () => { setTimeout(() => { ref.current += 1; }, 1000); }; return ( <div> {ref.current} <br /> <button type="button" onClick={onClick}> count up </button> </div> ); }; export default App;
ref 属性
HTML 要素やクラスコンポーネントにはref
という特別な属性を設定することができ、この属性にはRef
オブジェクトか関数を渡すことが出来る。
まずはRef
オブジェクトを渡すパターンから見ていく。
HTML 要素のref
属性にRef
オブジェクトを渡すと、Ref.current
にその HTML 要素が格納される。
import React, {useRef, useEffect} from 'react'; const App = () => { const ref = useRef(); useEffect(() => { console.log(ref.current.childNodes); // NodeList(2) [span, span] console.log(ref.current.firstChild.textContent); // text1 }); return ( <div ref={ref}> <span>text1</span> <span>text2</span> </div> ); }; export default App;
格納されるタイミングは、当該要素がマウントされた後。それまではcurrent
にはRef
オブジェクトの初期値が入っている。
そして、対象の要素がアンマウントされるとnull
が格納される。既述のようにuseRef
に引数を渡さなかった場合の初期値はundefined
なので、以下のような挙動になる。useRef(null)
とすれば、「要素がマウントされていないときの値はnull
」に統一できる。
import React, {useRef, useEffect, useState} from 'react'; const App = () => { const [state, setState] = useState(false); const ref = useRef(); useEffect(() => { // state が true のときは span 要素 // state が false のときは null // 但し初回レンダー時のみ undefined console.log(ref.current); }); const onClick = () => { setState(s => !s); }; return ( <div> <button type="button" onClick={onClick}> click </button> {state && <span ref={ref}>text1</span>} </div> ); }; export default App;
要素が再レンダーされたら、その変更も反映される。マウント時の値がそのまま保持されるわけではない。
import React, {useRef, useEffect, useState} from 'react'; const App = () => { const [count, setCount] = useState(0); const ref = useRef(); useEffect(() => { console.log(ref.current.textContent); // count の値 }); const onClick = () => { setCount(c => c + 1); }; return ( <div> <button type="button" onClick={onClick}> click </button> <span ref={ref}>{count}</span> </div> ); }; export default App;
クラスコンポーネントにもref
属性を設定することができ、その場合はクラスコンポーネントのインスタンスを得られる。
なので、ref.current
を通じて、そのコンポーネントのプロパティやメソッドにアクセスできる。
下記の例ではApp
が、Child
のプロパティx
やメソッドmethodA
にアクセスしている。
import React, {useRef, useEffect} from 'react'; const App = () => { const ref = useRef(); useEffect(() => { console.log(ref.current.x); // 1 }); const onClick = () => { ref.current.methodA(); // called methodA }; return ( <div> <button type="button" onClick={onClick}> click </button> <Child ref={ref} /> </div> ); }; class Child extends React.Component { constructor() { super(); this.x = 1; this.methodA = this.methodA.bind(this); } methodA() { console.log('called methodA'); } render() { return <span>child text</span>; } } export default App;
ref.current
の値の移り変わりは HTML 要素のときと同じであり、コンポーネントがアンマウントされたらやはりnull
になる。
ref
は特殊な属性であり、ref
を渡されたコンポーネントのprops.ref
でアクセスすることは出来ない。
例えば<Child ref={ref} value={1} />
としても、Child
のprops
にはvalue
しか含まれない。
ref
属性を設定することが出来るのは HTML 要素とクラスコンポーネントのみであり、関数コンポーネントに設定することは出来ない。
ref 属性に関数を渡す
既に述べたように、Ref
オブジェクトの内容が更新されても、それが通知されることはない。
そのため、ref
属性を設定している要素の状態が変わったタイミングで何かを発火させる、といったことが出来ない。
そのようなことを行いたい場合は、ref
属性に関数を渡す。この関数、及びこれを利用した手法を、「コールバック Ref」と呼ぶ。
この関数は、それを渡した要素のマウント時、更新時、アンマウント時に、実行される。
ref
属性にRef
オブジェクトを渡していたときはRef.current
の書き換えが発生していたが、その代わりにref
属性に渡した関数の実行が発生する。
そして、Ref
オブジェクトの場合はRef.current
に格納されていた値が、関数の引数として渡される。
下記の例では、ボタンを押下する度にspan
要素のマウントとアンマウントが交互に行われる。
その度にcallbackRef
が実行され、マウント時にはspan
要素が、アンマウント時にはnull
が、arg
に渡される。
import React, {useState} from 'react'; const App = () => { const [state, setState] = useState(false); const onClick = () => { setState(s => !s); }; const callbackRef = arg => { console.log(arg); // <span>text1</span> と null が交互に表示される }; return ( <div> <button type="button" onClick={onClick}> click </button> {state && <span ref={callbackRef}>text1</span>} </div> ); }; export default App;
まとめ
既に述べたように、値が変わり得るミュータブルな値を内部に抱えるのは、React の基本的なコンセプトから外れてしまう。
また、ref
属性を渡して要素を操作するのも、props
とstate
によって制御する、という基本的な考え方から逸脱してしまっている。
参考:
numb86-tech.hatenablog.com
そのため、Ref
の使用は基本的に避けるべき。公式ドキュメントでも、Ref
に言及する部分では、「こういう使い方が可能だけど、原則としてやるべきではない」という趣旨のことがよく書かれている。
とはいえ、Ref
が有用なケースもある。
例えば公式ドキュメントでは、アクセシビリティを実現するための手段としてref
属性を使っている。