かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React Hooks 親コンポーネントから子コンポーネントをのDOMを ref を使って触りたい。

React Hooks を使ったアプリでボタンの状態やフィールドへのフォーカスなど ref を使った操作を、そのボタンやフィールドの親コンポーネントから呼び出したい時のメモ。

日本語が不自由なのでサンプルを
例えばボタンを押した時に非同期処理をするから、処理が完了するまでボタンを disabled にしておきたいみたいな時

単純に useRef したもの props で渡しても上手くいかない

親コンポーネント

import React, { useRef } from 'react';
import SubmitButton from './SubmitButton';

function PostList(props) {
  const submitBtn = useRef(null);

  const onSubmitHandler = async (e) => {
    submitBtn.current.disabled = true;
    try {
      // ...非同期処理
    } catch (err) { ... }
    submitBtn.current.disabled = false;
  };

  return (
    <>
      {/* ...処理 */}
      <SubmitButton
        ref={submitBtn}
        onClick={onSubmitHandler}
      />
    </>
  );  
}

SubmitButton.js

import React from 'react';

function SubmitButton(props) {
  return (
    <button
      className="p-submitBtn"
      ref={props.ref}
      onClick={props.onClick}
    />SUBMIT</button>
  );
}
export default SubmitButton;

useRef したものを props で渡してボタンの ref 属性に付けても親コンポーネントでは null になってしまいうまくいかない。
上記の例ではボタンを押した時に実行される onSubmitHandler 内で submitBtn.current が null なのでエラーになってしまい意図したとおりに動作しない。

  const onSubmitHandler = async (e) => {
    // submitBtn.current は null
    submitBtn.current.disabled = true;
    // ...
  };

Forwarding Refs を使う

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef
ref. Hooks API Reference – React #useimperativehandle

React Hooks では子コンポーネント内で useImperativeHandle を使い ref.current から呼び出せる関数を定義して、子コンポーネント自体を forwardRef() でに渡しておくことで親コンポーネントのref.current を通じて子コンポーネントの ref にアクセスすることができる

親コンポーネント

import React, { useRef } from 'react';
import SubmitButton from './SubmitButton';

function PostList(props) {
  const submitBtn = useRef(null);

  const onSubmitHandler = async (e) => {
    // ref.current を通じて関数を呼び出すように変更
    submitBtn.current.disabled(false);
    try {
      // ...非同期処理
    } catch (err) { ... }
    submitBtn.current.disabled(false);
  };

  return (
    <>
      {/* ...処理 */}
      <SubmitButton
        ref={submitBtn}
        onClick={onSubmitHandler}
      />
    </>
  );  
}

SubmitButton.js

import React, { useRef, forwardRef, useImperativeHandle } from 'react';

function SubmitButton(props, ref) {
  const btnRef = useRef();

  useImperativeHandle(ref, () => {
    // 親コンポーネントの ref.current から実行できる関数を定義したオブジェクトを返す
    return {
      disabled: (flg) => btnRef.current.disabled = !!flg
    }
  });

  return (
    <button
      className="p-submitBtn"
      ref={btnRef}
      onClick={props.onClick}
    />SUBMIT</button>
  );
}
// コンポーネントを forwardRef でラップ
SubmitButton = forwardRef(SubmitButton);
export default SubmitButton;

forwardRef を使う場合 const で関数定義するほうが見やすいかも?

const SubmitButton = React.forwardRef((props, ref) => {
  // ...
});

これでボタンをクリックした時に、非同期処理が完了するまでボタンを disabled できるようになりました。

ポエム

useImperativeHandle で返すオブジェクトに setter として関数を定義したら親コンポーネントからプロパティみたいにアクセスできるのかな? チョット気になってきたので、時間があるときにでも試してみたい。

react-create-app もデフォルトで Function Component になったし、GW中にだいぶ React Hooks と仲良くなれたので、まとめたものを近々書いておきたい。 (まだ触れてない Hooks もたくさんあるけど…


[参考]

大人用品… og:image ネタのためだけに👆貼ったけどリスキーだったかな…