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

コンポーネントの要素から絶対座標、つまりウィンドウの左上角を起点とした座標が必要になりました。そのためには、要素の参照を得なければなりません。用いるフックは[`useRef`](https://ja.reactjs.org/docs/hooks-reference.html#useref)です。

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


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

```terminal
npx create-react-app react-useref-usewindowsize
```

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

```terminal
cd react-useref-usewindowsize
npm start
```

#### 図001■Reactアプリケーションのひな形ページ
![qiita_12_002_004.png](https://qiita-image-store.s3.amazonaws.com/0/262045/6e2d425e-205a-d520-0437-129811038bf3.png)


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

```react: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`で包みました。

```react: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`)の定めを加えます。

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

}

```


# useRefフックで要素の参照から絶対座標を求める
Reactから用いるフックは、つぎの3つです。

* [`useRef`](https://ja.reactjs.org/docs/hooks-reference.html#useref): 回転するロゴのコンポーネント(`rotatingLogoRef`)の参照を得ます。
* 
[`useEffect`](https://ja.reactjs.org/docs/hooks-reference.html#useeffect): Reactが描画を終えたとき行うべき処理を関数として定めます。
* [`useState`](https://ja.reactjs.org/docs/hooks-reference.html#usestate): 回転するロゴのコンポーネントから求めた絶対座標を状態変数に収めます。

要素から絶対座標値群を求めるのが、[`Element.getBoundingClientRect()`](https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect)メソッドです。モジュール`src/App.js`では、[オブジェクトの分割代入](https://qiita.com/FumioNonaka/items/58358a29850afd7a0f37#分割代入を使う)で4つのプロパティ値(`left`、`top`、`right`、`bottom`)を取り出しました。なお、`useRef`フックが返すのは、厳密には`ref`オブジェクトです。要素の参照は、`current`プロパティから得なければなりません。

```react: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)。

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

		</div>
	);
};
```

#### 図002■要素の絶対座標がページに示される
![2102001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/37f8da9f-f152-769a-04d3-0e0624f1c680.png)


# ウィンドウのサイズ変更に依存して再計算させる
前掲アプリケーションモジュール`src/App.js`の`useEffect`フックで、第2引数には空の配列`[]`を渡しました。この引数は依存配列と呼ばれ、配列要素の値が変わると改めて処理し直されます。

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

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

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

> `useRef`は中身が変更になってもそのことを通知しないということを覚えておいてください。`.current`プロパティを書き換えても再レンダーは発生しません。
> (「[useRef](https://ja.reactjs.org/docs/hooks-reference.html#useref)」)

ウィンドウのサイズ変更は、標準JavaScriptであれば[`resize`](https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event)イベントで捉えることになるでしょう。けれど、今回は[react-use](https://github.com/streamich/react-use#--------------------react-use------------------)の[`useWindowSize`](https://github.com/streamich/react-use/blob/master/docs/useWindowSize.md)フックを使ってみることにします。

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

```terminal
npm install react-use
```

```terminal
yarn add react-use
```

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

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

function App() {

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

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

}
```

書き上がったふたつのモジュールの記述は、以下にまとめたコード001のとおりです。併せて、[CodeSandboxにサンプルを掲げます](https://codesandbox.io/s/optimistic-varahamihira-r9v3d?file=/src/index.js)ので、詳しいコードや動きはこちらでお確かめください。

#### コード001■ウィンドウのサイズ変更に応じて要素の絶対座標を計算し直す
```react: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;
```

```react: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;
```