ツクログ

tsukulognet

tsukulognet

道産子。Reactでなまら面白いものを作りたい。

【React hooks】要素の変化を検知して何かをするカスタムフックを作成する

eye catch

useMutationObserver

useMutationObserver.js

import { useEffect } from 'react'

const useMutationObserver = (elements, callback, config) => {
    useEffect(() => {
        const mutationObserver = new MutationObserver((mutations) => {
            mutationObserver.disconnect();
            callback(mutations);
            for (const elem of elements) {
                elem.current && mutationObserver.observe(elem.current, config);
            }
        });

        for (const elem of elements) {
            elem.current && mutationObserver.observe(elem.current, config);
        }

        return () => mutationObserver.disconnect();
    }, []);
};

export default useMutationObserver

フックを呼び出す際、仮引数elementsには変更を検知したい要素を配列で入れます。また、仮引数callbackには何らかの処理を行う関数を入れ、仮引数configにはMutationObserverのオプションをオブジェクト形式で入れます。

MutationObserverのobserveメソッドでelements配列内の要素を監視します。監視中に要素の変化を検知するとcallback関数が呼び出さ、何らかの処理が行われます。要素の変化とは、要素に指定されているclassやid、style等の属性の変化や、要素自身の追加・変更・削除等による変化のことをいいます。

MutationObserverクラスのインスタンス化の際に、コールバック関数内で

mutationObserver.disconnect();
callback(mutations);
for (const elem of elements) {
  elem.current && mutationObserver.observe(elem.current, config);
}

のように、callback関数を実行する前に、disconnectメソッドを呼び出して要素の監視を止め、callback関数を実行した後にobserveメソッドを呼び出して再度監視を始めていますが、これはcallback内で要素の属性や子要素等を変更する処理を行ったときに起こる無限ループを回避するための対策です。

使用例

useMutationObserverフックをAppコンポーネント内で使うと以下のようになります。

App.js

const App = () => {
    const [left, setLeft] = useState(0)
    const [count, setCount] = useState(0)
    const element = useRef(null)
    const style = {
        backgroundColor: "red",
        height: `40vmin`,
        position: `relative`,
        left: `${count}px`,
        width: `40vmin`
    }

    const handleUpdatePosition = (mutations) => {
        const left = mutations[0].target.getBoundingClientRect().left
        setLeft(left)
    }

    useMutationObserver(
        handleUpdatePosition, 
        element.current, 
        { attributes: true }
    )

    useEffect(() => {
        setInterval(() => setCount(prev => prev + 1), 1000)
    }, [])

    return (
        <div className="App">
            <div>left: {left}</div>
            <div 
                ref={element}
                style={style}
            ></div>
        </div>
    )
}

上記の例では、elementを監視しており、elementはsetIntervalによって1秒ごとに右へ1pxずつ移動していき、移動を検知する度に現在の位置がページ上に表示されます。

つまり、このときMutationObserverは要素のstyle属性に指定されているスタイルのleftプロパティに指定されている値countの変化を1秒ごとに検知していることになります。

DEMO

下記で実際の動作を確認できます。

参考文献