Skip to content

Commit f597a4f

Browse files
committed
refactor: revert to useCallback for ref
Change it so cleanup is now done inside a `useEffect`. This allows the `setRef` to be called multiple times before the hook triggers the cleanup, ensuring the current `ref` is settled.
1 parent 0ae3f1c commit f597a4f

1 file changed

Lines changed: 32 additions & 18 deletions

File tree

src/useInView.tsx

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function useInView({
4545
fallbackInView,
4646
onChange,
4747
}: IntersectionOptions = {}): InViewHookResponse {
48-
const [ref, setRef] = React.useState<Element | null>(null);
48+
const ref = React.useRef<Element | null>(null);
4949
const unobserve = React.useRef<Function>();
5050
const callback = React.useRef<IntersectionOptions['onChange']>();
5151
const [state, setState] = React.useState<State>({
@@ -54,21 +54,28 @@ export function useInView({
5454
});
5555

5656
// Store the onChange callback in a `ref`, so we can access the latest instance
57-
// inside the `useEffect`, but without triggering a rerender.
57+
// inside the `useCallback`, but without triggering a rerender.
5858
callback.current = onChange;
5959

60-
React.useEffect(
61-
() => {
60+
const setRef = React.useCallback(
61+
(node: Element) => {
62+
ref.current = node;
6263
// Ensure we have node ref, and that we shouldn't skip observing
63-
if (skip || !ref) return;
64+
if (skip || !node) return;
6465

66+
// Store a reference the current unobserve function, so we can destroy it later
67+
const previousObserver = unobserve.current;
68+
69+
// Create a new IntersectionObserver, and store the `unobserve` function.
6570
unobserve.current = observe(
66-
ref,
71+
node,
6772
(inView, entry) => {
6873
setState({
6974
inView,
7075
entry,
7176
});
77+
78+
// Trigger any onChange callback function
7279
if (callback.current) callback.current(inView, entry);
7380

7481
if (entry.isIntersecting && triggerOnce && unobserve.current) {
@@ -89,20 +96,18 @@ export function useInView({
8996
fallbackInView,
9097
);
9198

92-
return () => {
93-
if (unobserve.current) {
94-
unobserve.current();
95-
unobserve.current = undefined;
96-
}
97-
};
99+
if (previousObserver) {
100+
// Was already observing a node - Make sure we destroy the previous observer.
101+
// Do it after we create the new one, so the IntersectionObserver instance can be reused.
102+
previousObserver();
103+
}
98104
},
99105
// We break the rule here, because we aren't including the actual `threshold` variable
100106
// eslint-disable-next-line react-hooks/exhaustive-deps
101107
[
102108
// If the threshold is an array, convert it to a string, so it won't change between renders.
103109
// eslint-disable-next-line react-hooks/exhaustive-deps
104110
Array.isArray(threshold) ? threshold.toString() : threshold,
105-
ref,
106111
root,
107112
rootMargin,
108113
triggerOnce,
@@ -113,18 +118,27 @@ export function useInView({
113118
],
114119
);
115120

116-
const entryTarget = state.entry?.target;
117-
121+
// We break the rule here, since we want to ensure we check the `ref` instances on every render.
122+
// eslint-disable-next-line react-hooks/exhaustive-deps
118123
React.useEffect(() => {
119-
if (!ref && entryTarget && !triggerOnce && !skip) {
124+
if (!ref.current && state.entry?.target && !triggerOnce && !skip) {
120125
// If we don't have a node ref, then reset the state (unless the hook is set to only `triggerOnce` or `skip`)
121-
// This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
126+
// This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView.
122127
setState({
123128
inView: !!initialInView,
124129
entry: undefined,
125130
});
126131
}
127-
}, [ref, entryTarget, triggerOnce, skip, initialInView]);
132+
133+
return () => {
134+
if (!ref.current && unobserve.current) {
135+
// We no longer have a valid ref. Destroy the observer
136+
unobserve.current();
137+
unobserve.current = undefined;
138+
ref.current = null;
139+
}
140+
};
141+
});
128142

129143
const result = [setRef, state.inView, state.entry] as InViewHookResponse;
130144

0 commit comments

Comments
 (0)