From 776caa6182826ea6735c8faed8691ab39312f16a Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Wed, 6 Jul 2022 11:17:52 +0200 Subject: [PATCH] feat: rewrite internals to use setState for `ref` --- .storybook/main.js | 2 +- src/stories/Hooks.story.tsx | 20 ++++++++++-- src/useInView.tsx | 64 ++++++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 26 deletions(-) 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/src/stories/Hooks.story.tsx b/src/stories/Hooks.story.tsx index 33f9108f..fd116d75 100644 --- a/src/stories/Hooks.story.tsx +++ b/src/stories/Hooks.story.tsx @@ -28,6 +28,7 @@ type Props = IntersectionOptions & { style?: CSSProperties; className?: string; lazy?: boolean; + inlineRef?: boolean; }; const story: Meta = { @@ -88,7 +89,13 @@ const story: Meta = { export default story; -const Template: Story = ({ style, className, lazy, ...rest }) => { +const Template: Story = ({ + style, + className, + lazy, + inlineRef, + ...rest +}) => { const { options, error } = useValidateOptions(rest); const { ref, inView, entry } = useInView(!error ? options : {}); const [isLoading, setIsLoading] = useState(lazy); @@ -109,7 +116,11 @@ const Template: Story = ({ style, className, lazy, ...rest }) => { return ( - + ref(node) : ref} + inView={inView} + style={style} + > @@ -127,6 +138,11 @@ LazyHookRendering.args = { lazy: true, }; +export const InlineRef = Template.bind({}); +LazyHookRendering.args = { + inlineRef: true, +}; + export const StartInView = Template.bind({}); StartInView.args = { initialInView: true, diff --git a/src/useInView.tsx b/src/useInView.tsx index c8dd8689..85d8e12a 100644 --- a/src/useInView.tsx +++ b/src/useInView.tsx @@ -45,29 +45,27 @@ export function useInView({ fallbackInView, onChange, }: IntersectionOptions = {}): InViewHookResponse { + const [ref, setRef] = React.useState(null); const unobserve = React.useRef(); 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`. callback.current = onChange; - const setRef = React.useCallback( - (node: Element | null) => { - if (unobserve.current !== undefined) { - unobserve.current(); - unobserve.current = undefined; - } - - // Skip creating the observer - if (skip) return; - - if (node) { + React.useEffect( + () => { + if (ref && !skip) { unobserve.current = observe( - node, + ref, (inView, entry) => { - setState({ inView, entry }); + setState({ + inView, + entry, + }); if (callback.current) callback.current(inView, entry); if (entry.isIntersecting && triggerOnce && unobserve.current) { @@ -87,12 +85,31 @@ export function useInView({ }, fallbackInView, ); + } else if (!ref && !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((prevState) => { + if (prevState.entry) { + return { + inView: !!initialInView, + entry: undefined, + }; + } + return prevState; + }); } + + return () => { + if (unobserve.current) { + unobserve.current(); + unobserve.current = undefined; + } + }; }, // 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, root, @@ -102,19 +119,20 @@ export function useInView({ trackVisibility, fallbackInView, delay, + ref, ], ); /* eslint-disable-next-line */ - 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`) - // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView - setState({ - inView: !!initialInView, - }); - } - }); + // 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`) + // // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView + // setState({ + // inView: !!initialInView, + // }); + // } + // }); const result = [setRef, state.inView, state.entry] as InViewHookResponse;