コンポーネントの要素から絶対座標、つまりウィンドウの左上角を起点とした座標が必要になりました。そのためには、要素の参照を得なければなりません。用いるフックは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アプリケーションのひな形ページ
ロゴの要素をコンポーネントに切り出す
絶対座標を求めるコンポーネントは、回転するロゴの部分にします。モジュールsrc/App.js
から別コンポーネント(RotatingLogo
)に切り分けましょう。
// 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
で包みました。
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
)の定めを加えます。
.App-logo {
width: 60vmin;
}
useRefフックで要素の参照から絶対座標を求める
Reactから用いるフックは、つぎの3つです。
-
useRef
: 回転するロゴのコンポーネント(rotatingLogoRef
)の参照を得ます。
useEffect
: Reactが描画を終えたとき行うべき処理を関数として定めます。
-
useState
: 回転するロゴのコンポーネントから求めた絶対座標を状態変数に収めます。
要素から絶対座標値群を求めるのが、Element.getBoundingClientRect()
メソッドです。モジュールsrc/App.js
では、オブジェクトの分割代入で4つのプロパティ値(left
、top
、right
、bottom
)を取り出しました。なお、useRef
フックが返すのは、厳密にはref
オブジェクトです。要素の参照は、current
プロパティから得なければなりません。
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)。
// const RotatingLogo = () => {
const RotatingLogo = ({ rotatingLogoRef }) => {
return (
// <div>
<div ref={rotatingLogoRef}>
</div>
);
};
図002■要素の絶対座標がページに示される
ウィンドウのサイズ変更に依存して再計算させる
前掲アプリケーションモジュールsrc/App.js
のuseEffect
フックで、第2引数には空の配列[]
を渡しました。この引数は依存配列と呼ばれ、配列要素の値が変わると改めて処理し直されます。
空の配列[]
は依存なしとみなされますので、コンポーネントがはじめて描画されたときしか実行されません。つまり、ウィンドウサイズを変えても、要素の座標は計算し直されないということです。
では、依存配列を適切に定めればよいでしょう。通常は、useEffect
フックの第1引数に定めた関数の中で、処理している値を拾って与えれば済みます。けれど、今回は適切な値がありません。
ref
オブジェクト(rotatingLogoRef
)あるいはそのcurrent
プロパティを依存配列に渡せればよさそうです。けれど、current
プロパティの変更は描画に影響を与えません。依存配列の監視対象として使えないのです。React公式サイトのリファレンスは、つぎのように注記しています。
useRef
は中身が変更になってもそのことを通知しないということを覚えておいてください。.current
プロパティを書き換えても再レンダーは発生しません。
(「useRef」)
ウィンドウのサイズ変更は、標準JavaScriptであればresize
イベントで捉えることになるでしょう。けれど、今回はreact-useのuseWindowSize
フックを使ってみることにします。
まず、npmもしくはyarnでreact-useをインストールしてください。
npm install react-use
yarn add react-use
useWindowSize
フックから返されるプロパティは、ウィンドウの幅(width
)と高さ(height
)です。これらの値をuseEffect
フックの依存配列に含めれば、ウィンドウサイズが変わるたびに要素の座標が計算し直されます。
import {useWindowSize} from 'react-use';
function App() {
const { width, height } = useWindowSize();
useEffect(() => {
// }, []);
}, [width, height]);
}
書き上がったふたつのモジュールの記述は、以下にまとめたコード001のとおりです。併せて、CodeSandboxにサンプルを掲げますので、詳しいコードや動きはこちらでお確かめください。
コード001■ウィンドウのサイズ変更に応じて要素の絶対座標を計算し直す
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;
import logo from './logo.svg';
const RotatingLogo = ({ rotatingLogoRef }) => {
return (
<div ref={rotatingLogoRef}>
<img src={logo} className="App-logo" alt="logo" />
</div>
);
};
export default RotatingLogo;