LoginSignup
8
6

More than 3 years have passed since last update.

React + React Use: ウィンドウのサイズ変更に応じて要素の絶対座標を計算し直す ー useRefとuseWindowSizeを使って

Posted at

コンポーネントの要素から絶対座標、つまりウィンドウの左上角を起点とした座標が必要になりました。そのためには、要素の参照を得なければなりません。用いるフックはuseRefです。

もうひとつ、ウィンドウのサイズを変えたとき、絶対座標は計算し直さなければなりません。今回は、便利フックの詰め合わせライブラリであるReact UseからuseWindowSizeを使ってみることにしました。

Create React App

Reactアプリケーションのひな型は、Create React Appでつくります。コマンドnpx create-react-appに続けて、プロジェクト名(今回はreact-useref-usewindowsizeとしました)を入力してください。

npx create-react-app react-useref-usewindowsize

プロジェクトのディレクトリに切り替えて、コマンドnpm startを入力すれば、ひな型のページが開くはずです(図001)。このひな型からサンプルとなる作例をつくりましょう。

cd react-useref-usewindowsize
npm start

図001■Reactアプリケーションのひな形ページ

qiita_12_002_004.png

ロゴの要素をコンポーネントに切り出す

絶対座標を求めるコンポーネントは、回転するロゴの部分にします。モジュールsrc/App.jsから別コンポーネント(RotatingLogo)に切り分けましょう。

src/App.js
// import logo from './logo.svg';
import RotatingLogo from './RotatingLogo';

function App() {
    return (
        <div className="App">
            <header className="App-header">
                {/* <img src={logo} className="App-logo" alt="logo" /> */}
                <RotatingLogo />

            </header>
        </div>
    );
}

回転するロゴのモジュールsrc/RotatingLogo.jsの記述はつぎのとおりです。座標が扱いやすいように、SVG(logo)のimg要素はdivで包みました。

src/RotatingLogo.js
import logo from './logo.svg';

const RotatingLogo = () => {
    return (
        <div>
            <img src={logo} className="App-logo" alt="logo" />
        </div>
    );
};

export default RotatingLogo;

やはり座標を扱うため、CSSのモジュールsrc/App.cssには、クラスApp-logoに幅(width)の定めを加えます。

src/App.css
.App-logo {
    width: 60vmin;

}

useRefフックで要素の参照から絶対座標を求める

Reactから用いるフックは、つぎの3つです。

  • useRef: 回転するロゴのコンポーネント(rotatingLogoRef)の参照を得ます。
  • useEffect: Reactが描画を終えたとき行うべき処理を関数として定めます。
  • useState: 回転するロゴのコンポーネントから求めた絶対座標を状態変数に収めます。

要素から絶対座標値群を求めるのが、Element.getBoundingClientRect()メソッドです。モジュールsrc/App.jsでは、オブジェクトの分割代入で4つのプロパティ値(lefttoprightbottom)を取り出しました。なお、useRefフックが返すのは、厳密にはrefオブジェクトです。要素の参照は、currentプロパティから得なければなりません。

src/App.js
import { useEffect, useRef, useState } from 'react';


function App() {
    const [rotatingLogoRect, setRotatingLogoRect] = useState({ left: 0, top: 0, right: 0, bottom: 0 });
    const rotatingLogoRef = useRef(null);
    useEffect(() => {
        const rotatingLogo = rotatingLogoRef.current;
        const { left, top, right, bottom } = rotatingLogo.getBoundingClientRect();
        setRotatingLogoRect({ left, top, right, bottom });
    }, []);
    return (
        <div className="App">
            <header className="App-header">
                {/* <RotatingLogo /> */}
                <RotatingLogo rotatingLogoRef={rotatingLogoRef} />
                <p>
                    {/* Edit <code>src/App.js</code> and save to reload. */}
                    left: {rotatingLogoRect.left}, top: {rotatingLogoRect.top}, right: {rotatingLogoRect.right}, bottom: {rotatingLogoRect.bottom}
                </p>


            </header>
        </div>
    );
}

useRefの戻り値は、参照を得たい要素のrefプロパティに与えなければなりません。親コンポーネント(App)から渡すプロパティ(rotatingLogoRef)を介して、モジュールsrc/RotatingLogo.jsにつぎのように定めましょう。これで、回転するロゴのコンポーネントの絶対座標がページに示されます(図002)。

src/RotatingLogo.js
// const RotatingLogo = () => {
const RotatingLogo = ({ rotatingLogoRef }) => {
    return (
        // <div>
        <div ref={rotatingLogoRef}>

        </div>
    );
};

図002■要素の絶対座標がページに示される

2102001_001.png

ウィンドウのサイズ変更に依存して再計算させる

前掲アプリケーションモジュールsrc/App.jsuseEffectフックで、第2引数には空の配列[]を渡しました。この引数は依存配列と呼ばれ、配列要素の値が変わると改めて処理し直されます。

空の配列[]は依存なしとみなされますので、コンポーネントがはじめて描画されたときしか実行されません。つまり、ウィンドウサイズを変えても、要素の座標は計算し直されないということです。

では、依存配列を適切に定めればよいでしょう。通常は、useEffectフックの第1引数に定めた関数の中で、処理している値を拾って与えれば済みます。けれど、今回は適切な値がありません。

refオブジェクト(rotatingLogoRef)あるいはそのcurrentプロパティを依存配列に渡せればよさそうです。けれど、currentプロパティの変更は描画に影響を与えません。依存配列の監視対象として使えないのです。React公式サイトのリファレンスは、つぎのように注記しています。

useRefは中身が変更になってもそのことを通知しないということを覚えておいてください。.currentプロパティを書き換えても再レンダーは発生しません。
(「useRef」)

ウィンドウのサイズ変更は、標準JavaScriptであればresizeイベントで捉えることになるでしょう。けれど、今回はreact-useuseWindowSizeフックを使ってみることにします。

まず、npmもしくはyarnでreact-useをインストールしてください。

npm install react-use
yarn add react-use

useWindowSizeフックから返されるプロパティは、ウィンドウの幅(width)と高さ(height)です。これらの値をuseEffectフックの依存配列に含めれば、ウィンドウサイズが変わるたびに要素の座標が計算し直されます。

src/App.js
import {useWindowSize} from 'react-use';

function App() {

    const { width, height } = useWindowSize();
    useEffect(() => {

    // }, []);
    }, [width, height]);

}

書き上がったふたつのモジュールの記述は、以下にまとめたコード001のとおりです。併せて、CodeSandboxにサンプルを掲げますので、詳しいコードや動きはこちらでお確かめください。

コード001■ウィンドウのサイズ変更に応じて要素の絶対座標を計算し直す

src/App.js
import { useEffect, useRef, useState } from 'react';
import {useWindowSize} from 'react-use';
import RotatingLogo from './RotatingLogo';
import './App.css';

function App() {
    const [rotatingLogoRect, setRotatingLogoRect] = useState({ left: 0, top: 0, right: 0, bottom: 0 });
    const rotatingLogoRef = useRef(null);
    const { width, height } = useWindowSize();
    useEffect(() => {
        const rotatingLogo = rotatingLogoRef.current;
        const { left, top, right, bottom } = rotatingLogo.getBoundingClientRect();
        setRotatingLogoRect({ left, top, right, bottom });
}, [width, height]);
return (
    <div className="App">
      <header className="App-header">
                <RotatingLogo rotatingLogoRef={rotatingLogoRef} />
        <p>
                    left: {rotatingLogoRect.left}, top: {rotatingLogoRect.top}, right: {rotatingLogoRect.right}, bottom: {rotatingLogoRect.bottom}
        </p>
        <a
          className="App-link"
          href="https://ja.reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
src/RotatingLogo.js
import logo from './logo.svg';

const RotatingLogo = ({ rotatingLogoRef }) => {
    return (
        <div ref={rotatingLogoRef}>
            <img src={logo} className="App-logo" alt="logo" />
        </div>
    );
};

export default RotatingLogo;
8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6