diff --git a/README.md b/README.md
index 75814a87..24ff9580 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,7 @@ argument for the hooks.
| **delay** 🧪 | number | undefined | false | A number indicating the minimum delay in milliseconds between notifications from this observer for a given target. This must be set to at least `100` if `trackVisibility` is `true`. |
| **skip** | boolean | false | false | Skip creating the IntersectionObserver. You can use this to enable and disable the observer as needed. If `skip` is set while `inView`, the current state will still be kept. |
| **triggerOnce** | boolean | false | false | Only trigger the observer once. |
+| **initialInView** | boolean | false | false | Set the initial value of the `inView` boolean. This can be used if you you expect the element to be in the viewport to start with, and you want to trigger something when it leaves. |
> ⚠️ When passing an array to `threshold`, store the array in a constant to
> avoid the component re-rendering too often. For example:
diff --git a/src/InView.tsx b/src/InView.tsx
index 2f356bb1..f1a1b7f2 100644
--- a/src/InView.tsx
+++ b/src/InView.tsx
@@ -24,14 +24,18 @@ export class InView extends React.Component<
static defaultProps = {
threshold: 0,
triggerOnce: false,
+ initialInView: false,
};
- state: State = {
- inView: false,
- entry: undefined,
- };
+ constructor(props: IntersectionObserverProps | PlainChildrenProps) {
+ super(props);
+ this.state = {
+ inView: !!props.initialInView,
+ entry: undefined,
+ };
+ }
- componentDidUpdate(prevProps: IntersectionObserverProps, prevState: State) {
+ componentDidUpdate(prevProps: IntersectionObserverProps) {
// If a IntersectionObserver option changed, reinit the observer
if (
prevProps.rootMargin !== this.props.rootMargin ||
@@ -83,7 +87,7 @@ export class InView extends React.Component<
if (!node && !this.props.triggerOnce && !this.props.skip) {
// Reset the state if we get a new node, and we aren't ignoring updates
- this.setState({ inView: false, entry: undefined });
+ this.setState({ inView: !!this.props.initialInView, entry: undefined });
}
}
this.node = node ? node : null;
@@ -124,6 +128,7 @@ export class InView extends React.Component<
skip,
trackVisibility,
delay,
+ initialInView,
...props
} = this.props;
diff --git a/src/__tests__/InView.test.tsx b/src/__tests__/InView.test.tsx
index 41d53695..947cc767 100644
--- a/src/__tests__/InView.test.tsx
+++ b/src/__tests__/InView.test.tsx
@@ -51,6 +51,17 @@ it('Should respect skip', () => {
expect(cb).not.toHaveBeenCalled();
});
+
+it('Should handle initialInView', () => {
+ const cb = jest.fn();
+ render(
+
+ {({ inView }) => `InView: ${inView}`}
+ ,
+ );
+ screen.getByText('InView: true');
+});
+
it('Should unobserve old node', () => {
const { rerender } = render(
diff --git a/src/__tests__/hooks.test.tsx b/src/__tests__/hooks.test.tsx
index dd4c2ddd..b8b6041e 100644
--- a/src/__tests__/hooks.test.tsx
+++ b/src/__tests__/hooks.test.tsx
@@ -57,6 +57,15 @@ test('should create a hook inView', () => {
getByText('true');
});
+test('should create a hook with initialInView', () => {
+ const { getByText } = render(
+ ,
+ );
+ getByText('true');
+ mockAllIsIntersecting(false);
+ getByText('false');
+});
+
test('should trigger a hook leaving view', () => {
const { getByText } = render();
mockAllIsIntersecting(true);
diff --git a/src/__tests__/observers.test.ts b/src/__tests__/observers.test.ts
index f89d4e3e..9d11004c 100644
--- a/src/__tests__/observers.test.ts
+++ b/src/__tests__/observers.test.ts
@@ -7,12 +7,31 @@ test('should convert options to id', () => {
rootMargin: '10px 10px',
threshold: [0, 1],
}),
- ).toMatchInlineSnapshot(`"root_1|rootMargin_10px 10px|threshold_0,1"`);
+ ).toMatchInlineSnapshot(`"root_1,rootMargin_10px 10px,threshold_0,1"`);
expect(
optionsToId({
root: null,
rootMargin: '10px 10px',
threshold: 1,
}),
- ).toMatchInlineSnapshot(`"root_|rootMargin_10px 10px|threshold_1"`);
+ ).toMatchInlineSnapshot(`"root_0,rootMargin_10px 10px,threshold_1"`);
+ expect(
+ optionsToId({
+ threshold: 0,
+ // @ts-ignore
+ trackVisibility: true,
+ // @ts-ignore
+ delay: 500,
+ }),
+ ).toMatchInlineSnapshot(`"delay_500,threshold_0,trackVisibility_true"`);
+ expect(
+ optionsToId({
+ threshold: 0,
+ }),
+ ).toMatchInlineSnapshot(`"threshold_0"`);
+ expect(
+ optionsToId({
+ threshold: [0, 0.5, 1],
+ }),
+ ).toMatchInlineSnapshot(`"threshold_0,0.5,1"`);
});
diff --git a/src/index.tsx b/src/index.tsx
index e4b56248..a584b083 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -31,6 +31,7 @@ export interface IntersectionOptions extends IntersectionObserverInit {
triggerOnce?: boolean;
/** Skip assigning the observer to the `ref` */
skip?: boolean;
+ initialInView?: boolean;
/** IntersectionObserver v2 - Track the actual visibility of the element */
trackVisibility?: boolean;
/** IntersectionObserver v2 - Set a minimum delay between notifications */
diff --git a/src/observers.ts b/src/observers.ts
index 59390879..f38fe49f 100644
--- a/src/observers.ts
+++ b/src/observers.ts
@@ -11,34 +11,35 @@ const ObserverMap = new Map<
const RootIds: Map = new Map();
-let consecutiveRootId = 0;
+let rootId = 0;
/**
* Generate a unique ID for the root element
* @param root
*/
function getRootId(root?: Element | null) {
- if (!root) return '';
+ if (!root) return '0';
if (RootIds.has(root)) return RootIds.get(root);
- consecutiveRootId += 1;
- RootIds.set(root, consecutiveRootId.toString());
+ rootId += 1;
+ RootIds.set(root, rootId.toString());
return RootIds.get(root);
}
/**
* Convert the options to a string Id, based on the values.
- * Ensures we can reuse the same observer for, when observer elements with the same options.
+ * Ensures we can reuse the same observer when observing elements with the same options.
* @param options
*/
export function optionsToId(options: IntersectionObserverInit) {
return Object.keys(options)
+ .filter(Boolean)
.sort()
.map((key) => {
return `${key}_${
key === 'root' ? getRootId(options.root) : options[key]
}`;
})
- .join('|');
+ .toString();
}
function createObserver(options: IntersectionObserverInit) {
diff --git a/src/stories/Hooks.story.tsx b/src/stories/Hooks.story.tsx
index d52da049..5f02c49c 100644
--- a/src/stories/Hooks.story.tsx
+++ b/src/stories/Hooks.story.tsx
@@ -83,7 +83,9 @@ export const lazyHookRendering = () => (
);
-export const startInView = () => ;
+export const startInView = () => (
+
+);
export const tallerThanViewport = () => (
diff --git a/src/useInView.tsx b/src/useInView.tsx
index 314e3c60..3f642d71 100644
--- a/src/useInView.tsx
+++ b/src/useInView.tsx
@@ -1,23 +1,27 @@
-/* eslint-disable react-hooks/exhaustive-deps */
import * as React from 'react';
import { InViewHookResponse, IntersectionOptions } from './index';
import { useEffect } from 'react';
import { observe } from './observers';
+
type State = {
inView: boolean;
entry?: IntersectionObserverEntry;
};
-const initialState: State = {
- inView: false,
- entry: undefined,
-};
-
-export function useInView(
- options: IntersectionOptions = {},
-): InViewHookResponse {
+export function useInView({
+ threshold,
+ delay,
+ trackVisibility,
+ rootMargin,
+ root,
+ triggerOnce,
+ skip,
+ initialInView,
+}: IntersectionOptions = {}): InViewHookResponse {
const unobserve = React.useRef();
- const [state, setState] = React.useState(initialState);
+ const [state, setState] = React.useState({
+ inView: !!initialInView,
+ });
const setRef = React.useCallback(
(node) => {
@@ -26,9 +30,8 @@ export function useInView(
unobserve.current = undefined;
}
- if (options.skip) {
- return;
- }
+ // Skip creating the observer
+ if (skip) return;
if (node) {
unobserve.current = observe(
@@ -36,36 +39,35 @@ export function useInView(
(inView, entry) => {
setState({ inView, entry });
- if (
- entry.isIntersecting &&
- options.triggerOnce &&
- unobserve.current
- ) {
+ if (entry.isIntersecting && triggerOnce && unobserve.current) {
// If it should only trigger once, unobserve the element after it's inView
unobserve.current();
unobserve.current = undefined;
}
},
- options,
+ {
+ root,
+ rootMargin,
+ threshold,
+ // @ts-ignore
+ trackVisibility,
+ // @ts-ignore
+ delay,
+ },
);
}
},
- [
- options.threshold,
- options.root,
- options.rootMargin,
- options.triggerOnce,
- options.skip,
- options.trackVisibility,
- options.delay,
- ],
+ [threshold, root, rootMargin, triggerOnce, skip, trackVisibility, delay],
);
+ /* eslint-disable-next-line */
useEffect(() => {
- if (!unobserve.current && !options.triggerOnce && !options.skip) {
+ if (!unobserve.current && state.entry && !triggerOnce && !skip) {
// If we don't have a ref, then reset the state (unless the hook is set to only `triggerOnce` or `skip`)
// This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
- setState(initialState);
+ setState({
+ inView: !!initialInView,
+ });
}
});