IntersectionObserverAPIをuseContext で どこでも使えるようにしてみた
最近、仕事でスクロールしてコンテンツがフェードインするインタラクションを作ったのですが色々勉強になることも多かったのでメモとして記述します。
IntersectionObserverAPI
スクロールしてふわっとしたフェードインを作りたい場合、Reactでは便利なライブラリとしてreact-intersection-observerというものがあるのですが今回はVanilla JSが使い慣れているということもあって普通にIntersectionObserverAPIを採用することにしました。
(めんどくさいと感じたらこっち使ってください↓)
import { useEffect, useRef } from "react";
import "./App.css";
function App() {
const options = {
//下20%見えてから関数を呼び出す
rootMargin: "-20% 0px",
once: false,
};
const targets = useRef([]);
const toTargets = (e) => {
if (e && !targets.current?.includes(e)) {
targets.current.push(e);
}
};
useEffect(() => {
targets.current.forEach((target) => {
const callback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-fadeActive");
} else {
if (!options.once) {
entry.target.classList.remove("is-fadeActive");
}
}
});
};
const observer = new IntersectionObserver(callback, options);
observer.observe(target);
});
}, [targets]);
return (
<div className="App">
<div ref={toTargets}>
<div className="grid">
<div className="yellow box"></div>
<div className="red box"></div>
<div className="blue box"></div>
</div>
</div>
</div>
);
}
export default App;
これでこのコンポーネント内のコンテンツのみスクロールされるとふわっとフェードインされるようになりました。
しかしながらコンポーネントだけじゃくてサイト全体の各コンテンツが同じようにフェードインするにはコンポーネントごとに同じ記述を繰り返し書く必要があります。各コンポーネントごとに細かいオプションをつけたかったら別々で書いても良いのですがそうでない限りコードの肥大化は防ぎたいところです。
そこでReact hooksのuseContextを使いどこでも使いまわせるようにします。
useContextとは?
コンポーネントが増えてくるとpropsによるデータの受け渡しが複雑になっていってその分コードも見えにくくなっていきます。そこでuseContextを利用することでグローバルなデータを作り、どこのコンポーネントでもそのデータを使えるようにできるのです。
使い方
流れとしては
→グローバルなデータを持つコンポーネントを作る
→Providerで全体を囲む
→使いたいコンポーネント箇所でuseContextを使って呼び出す。
の3つの手順になります。
もう少し細分化してみましょう
グローバルなデータを持つコンポーネントを作る
1.フォルダーを作り、その中に今回使いまわしたいIntersectionObserverのコンポーネントを作る。
2.createContextを作ってコンテキストのオプジェクトを作る
3.Providerを用いて内部のコンポーネント(今回であればchildren)にvalueのデータを渡せるようにする
4.最後に使いたい場所でuseContextを呼び出してその中にどのコンテキスト(createContextで定義したもの)を使いたいのか選ぶ。
import React, { createContext, useEffect, useRef } from "react";
//createContextオブジェクトを用意する
export const ObserverContext = createContext({});
export function IntersectionObserverProvider(props) {
const { children } = props;
//グローバルな関数を準備する
const options = {
//下20%見えてから関数を呼び出す
rootMargin: "-20% 0px",
once: false,
};
const targets = useRef([]);
const toTargets = (e) => {
if (el && !targets.current?.includes(e)) {
targets.current.push(e);
}
};
useEffect(() => {
targets.current.forEach((target) => {
const callback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-fadeActive");
} else {
if (!options.once) {
entry.target.classList.remove("is-fadeActive");
}
}
});
};
const observer = new IntersectionObserver(callback, options);
observer.observe(target);
});
}, [targets]);
return (
<>
<ObserverContext.Provider value={{ toTargets }}>{children}</ObserverContext.Provider>
</>
);
}
親のコンポーネントでProviderで囲むことによってグローバルなデータが子のコンポーネント内で使えるようになります。親のコンポーネント↓
import "./App.css";
import { BoxItems } from "./components/BoxItems/BoxItems";
import { IntersectionObserverProvider } from "./components/provider/IntersectionObserverProvider";
function App() {
return (
<IntersectionObserverProvider>
<div className="App">
<BoxItems />
</div>
</IntersectionObserverProvider>
);
}
export default App;
import { useContext } from "react";
import { ObserverContext } from "../provider/IntersectionObserverProvider";
export const BoxItems = () => {
const { toTargets } = useContext(ObserverContext);
return (
<div ref={toTargets}>
<div className="grid">
<div className="yellow box"></div>
<div className="red box"></div>
<div className="blue box"></div>
</div>
</div>
);
};
いかがでしたか?確かに便利ですかね。
グローバルなデータをコンテキストを用いることでコードの肥大化がなくなって多少は見やすくなるかと思います。ただuseContextって書き方が冗長に感じたり、再レンダリングの懸念とかあるので少し慣れるまで時間がかるかなって感じました。
まあこの辺りはRecoilというもので書き換えたらいいかなって考えていて時間がある時に書き換えてご説明したいと思います。
参考
Discussion