diff --git a/.eslintrc.js b/.eslintrc.js
index 7cbe9d0e..2385fdd2 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -91,7 +91,7 @@ module.exports = {
'no-this-before-super': 'warn',
'no-throw-literal': 'warn',
'no-unexpected-multiline': 'warn',
- 'no-unreachable': 'wa' + 'rn',
+ 'no-unreachable': 'warn',
'no-unused-expressions': [
'error',
{
diff --git a/.storybook/main.js b/.storybook/main.js
index dd1404ae..4c2991e9 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -30,7 +30,7 @@ module.exports = {
async viteFinal(config) {
// The build fails to correctly minify the `ansi-to-html` module with esbuild, so we fallback to Terser.
// It's a package used by "Storybook" for the Webpreview, so it's interesting why it fails.
- config.build.minify = 'terser';
+ if (config.build) config.build.minify = 'terser';
if (config.optimizeDeps) {
config.optimizeDeps.include = [
diff --git a/README.md b/README.md
index 50baff71..8ec7a2ce 100644
--- a/README.md
+++ b/README.md
@@ -165,9 +165,9 @@ Provide these as the options argument in the `useInView` hook or as props on the
The **``** component also accepts the following props:
-| Name | Type | Default | Description |
-| ------------ | ---------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **as** | `string` | `'div'` | Render the wrapping element as this element. Defaults to `div`. |
+| Name | Type | Default | Description |
+| ------------ | ---------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| **as** | `string` | `'div'` | Render the wrapping element as this element. Defaults to `div`. |
| **children** | `({ref, inView, entry}) => ReactNode` or `ReactNode` | `undefined` | Children expects a function that receives an object containing the `inView` boolean and a `ref` that should be assigned to the element root. Alternatively pass a plain child, to have the `` deal with the wrapping element. You will also get the `IntersectionObserverEntry` as `entry`, giving you more details. |
### Intersection Observer v2 🧪
@@ -218,9 +218,9 @@ import { useInView } from 'react-intersection-observer';
function Component(props) {
const ref = useRef();
- const [inViewRef, inView] = useInView();
+ const { ref: inViewRef, inView } = useInView();
- // Use `useCallback` so we don't recreate the function on each render - Could result in infinite loop
+ // Use `useCallback` so we don't recreate the function on each render
const setRefs = useCallback(
(node) => {
// Ref's from useRef needs to have the node assigned to `current`
diff --git a/src/stories/Hooks.story.tsx b/src/stories/Hooks.story.tsx
index 33f9108f..5f90754b 100644
--- a/src/stories/Hooks.story.tsx
+++ b/src/stories/Hooks.story.tsx
@@ -1,4 +1,3 @@
-import { action } from '@storybook/addon-actions';
import { Meta, Story } from '@storybook/react';
import { IntersectionOptions, InView, useInView } from '../index';
import { motion } from 'framer-motion';
@@ -28,6 +27,7 @@ type Props = IntersectionOptions & {
style?: CSSProperties;
className?: string;
lazy?: boolean;
+ inlineRef?: boolean;
};
const story: Meta = {
@@ -79,6 +79,7 @@ const story: Meta = {
table: {
disable: true,
},
+ action: 'InView',
},
},
args: {
@@ -88,11 +89,19 @@ const story: Meta = {
export default story;
-const Template: Story = ({ style, className, lazy, ...rest }) => {
+const Template: Story = ({
+ style,
+ className,
+ lazy,
+ inlineRef,
+ ...rest
+}) => {
+ // const onChange: IntersectionOptions['onChange'] = (inView, entry) => {
+ // action('InView')(inView, entry);
+ // }
const { options, error } = useValidateOptions(rest);
- const { ref, inView, entry } = useInView(!error ? options : {});
+ const { ref, inView } = useInView(!error ? { ...options } : {});
const [isLoading, setIsLoading] = useState(lazy);
- action('InView')(inView, entry);
useEffect(() => {
if (isLoading) setIsLoading(false);
@@ -109,7 +118,11 @@ const Template: Story = ({ style, className, lazy, ...rest }) => {
return (
-
+ ref(node) : ref}
+ inView={inView}
+ style={style}
+ >
@@ -127,6 +140,11 @@ LazyHookRendering.args = {
lazy: true,
};
+export const InlineRef = Template.bind({});
+InlineRef.args = {
+ inlineRef: true,
+};
+
export const StartInView = Template.bind({});
StartInView.args = {
initialInView: true,
diff --git a/src/useInView.tsx b/src/useInView.tsx
index c8dd8689..a7a89a04 100644
--- a/src/useInView.tsx
+++ b/src/useInView.tsx
@@ -45,56 +45,62 @@ export function useInView({
fallbackInView,
onChange,
}: IntersectionOptions = {}): InViewHookResponse {
- const unobserve = React.useRef();
+ const [ref, setRef] = React.useState(null);
const callback = React.useRef();
const [state, setState] = React.useState({
inView: !!initialInView,
+ entry: undefined,
});
- // Store the onChange callback in a `ref`, so we can access the latest instance inside the `useCallback`.
+
+ // Store the onChange callback in a `ref`, so we can access the latest instance
+ // inside the `useEffect`, but without triggering a rerender.
callback.current = onChange;
- const setRef = React.useCallback(
- (node: Element | null) => {
- if (unobserve.current !== undefined) {
- unobserve.current();
- unobserve.current = undefined;
- }
+ React.useEffect(
+ () => {
+ // Ensure we have node ref, and that we shouldn't skip observing
+ if (skip || !ref) return;
- // Skip creating the observer
- if (skip) return;
+ let unobserve: (() => void) | undefined = observe(
+ ref,
+ (inView, entry) => {
+ setState({
+ inView,
+ entry,
+ });
+ if (callback.current) callback.current(inView, entry);
- if (node) {
- unobserve.current = observe(
- node,
- (inView, entry) => {
- setState({ inView, entry });
- if (callback.current) callback.current(inView, entry);
+ if (entry.isIntersecting && triggerOnce && unobserve) {
+ // If it should only trigger once, unobserve the element after it's inView
+ unobserve();
+ unobserve = undefined;
+ }
+ },
+ {
+ root,
+ rootMargin,
+ threshold,
+ // @ts-ignore
+ trackVisibility,
+ // @ts-ignore
+ delay,
+ },
+ fallbackInView,
+ );
- if (entry.isIntersecting && triggerOnce && unobserve.current) {
- // If it should only trigger once, unobserve the element after it's inView
- unobserve.current();
- unobserve.current = undefined;
- }
- },
- {
- root,
- rootMargin,
- threshold,
- // @ts-ignore
- trackVisibility,
- // @ts-ignore
- delay,
- },
- fallbackInView,
- );
- }
+ return () => {
+ if (unobserve) {
+ unobserve();
+ }
+ };
},
// We break the rule here, because we aren't including the actual `threshold` variable
// eslint-disable-next-line react-hooks/exhaustive-deps
[
- // If the threshold is an array, convert it to a string so it won't change between renders.
+ // If the threshold is an array, convert it to a string, so it won't change between renders.
// eslint-disable-next-line react-hooks/exhaustive-deps
Array.isArray(threshold) ? threshold.toString() : threshold,
+ ref,
root,
rootMargin,
triggerOnce,
@@ -105,16 +111,18 @@ export function useInView({
],
);
- /* eslint-disable-next-line */
+ const entryTarget = state.entry?.target;
+
React.useEffect(() => {
- 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`)
+ if (!ref && entryTarget && !triggerOnce && !skip) {
+ // If we don't have a node 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({
inView: !!initialInView,
+ entry: undefined,
});
}
- });
+ }, [ref, entryTarget, triggerOnce, skip, initialInView]);
const result = [setRef, state.inView, state.entry] as InViewHookResponse;