React - Ref
Refs and the DOM
「宣言的」と「命令的」の違い
- 宣言的 (declarative)
- プロパティで制御する
isOpen
など - Controlled Component という
- プロパティで制御する
- 命令的 (imperative)
- メソッドで制御する
open()
など - 子側で勝手に操作される
- Uncontrolled Component という
- メソッドで制御する
宣言的に子コンポーネントを操作できる場合は、必ずそうすること。 宣言的には操作できない場合や、フォームのネイティブの動作を尊重したい場合など、ごく限られた場面においては命令的に子コンポーネントを操作する。この場合に使うのが、Ref である。
いつ Ref を使うべきか
- フォーカスを扱う時、テキストを選択するとき、メディアを再生するとき
- アニメーションを発動するとき
- サードパーティの DOM ライブラリを React で使う時
Ref の作成
React.createRef()
で作成し、要素のref
属性に設定する。ref
属性はclassName
などと同じように、他のカスタムな属性とは異なる特別な役割を持っている。
コンポーネント全体で利用できるように、インスタンスプロパティに代入しておくのが一般的。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
Ref へのアクセス
ref を 要素に設定すると、this.myRef.current
のような形で要素にアクセスできるようになる。この値は、ref を設定した要素の種類によって異なってくる。
- HTML 要素の場合(
div
など) --- HTML 要素自体 - クラスコンポーネントの場合(
MyComponent
など) --- クラスコンポーネントのインスタンス - ファンクションコンポーネントの場合 --- 使用不可、ただしHook を使用すれば可能
タイミング
コンポーネントがマウントされると React はcurrent
に設定された変数に値を割り当て、マウント解除されるとそれを null
に戻す。
current
の値の更新は、componentDidMount または componentDidUpdate ライフサイクルメソッドの前に行われる。
Callback Refs
Ref を設定するもう一つの方法。より細かい制御が可能。
ref
属性に、createRef()
で作った値ではなく、関数を渡す。
この関数は、コンポーネントがマウント・アンマウントされたときに呼ばれる。マウントされたときは DOM 要素またはインスタンスとともに呼ばれる。アンマウントされたときはnull
とともに呼ばれる。
Ref は、componentDidMount や componentDidUpdate が呼ばれる前に実行されるので、常に最新であることが保証される。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
}
setTextInputRef = (element) => {
this.textInput = element;
};
focusTextInput = () => {
if (this.textInput) this.textInput.focus();
};
render = () => {
return (
<div>
<input type="text" ref={this.setTextInputRef} />
<input onClick={this.focusTextInput} />
</div>
);
};
}
子コンポーネントの Ref を取得する
例えば下記の例では、inputRef
という属性に設定している Callback Ref により、子の Ref を取得できる。
なお、inputRef
のようなカスタムな属性ではなく、ref
属性で子コンポーネントの特定の要素(インスタンスではない)を取得したい場合は、後述の Ref Forwarding を使用すること。
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return <CustomTextInput inputRef={(el) => (this.inputElement = el)} />;
}
}
Ref Forwarding
Ref を DOM に転送する
あるコンポーネントが、受け取ったref
をコンポーネント内の別の DOM 要素に割り当てる(転送する)場合に使う 。React.forwardRef()
を使うことで実現する。ごく限られた場面で使用し、乱用しないこと。
下記の例では、FancyButton
は、さもbutton
要素であるかのように振る舞うことができる。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref}>{props.children}</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
なお、クラスコンポーネントでは下記のようにする。
class FancyButton extends Component {
render() {
return <button ref={this.props.innerRef}>{this.props.children}</button>;
}
}
export default React.forwardRef((props, ref) => (
// カスタム属性で渡し ている点に注意する。
// もし`ref`属性に設定すると、ref=FancyButtonクラスのインスタンスになってしまう。
<FancyButton innerRef={ref} {...props} />
));
ライブラリ作成者に向けての注意
- forwardRef を使い始めるときは、Breaking Change として扱うべし
- forwardRef の適用を Conditional に決定することは避けるべし
HOC と forwardRef を組み合わせて使う方法
HOC で forwardRef を使いたい場合は、ref
属性を手動で設定する必要がある。
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const { forwardedRef, ...rest } = this.props;
// Assign the custom prop "forwardedRef" as a ref
return <Component ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
// forwardedRefというカスタム属性を使っている点に注目。
// ここでref属性として渡してしまうと、ref=LogPropsのインスタンスになってしまう。
return <LogProps {...props} forwardedRef={ref} />;
});
}