Skip to content

Commit

Permalink
feat: rewrite internals to use setState for ref
Browse files Browse the repository at this point in the history
  • Loading branch information
thebuilder committed Jul 6, 2022
1 parent ae5dcf6 commit 776caa6
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
20 changes: 18 additions & 2 deletions src/stories/Hooks.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Props = IntersectionOptions & {
style?: CSSProperties;
className?: string;
lazy?: boolean;
inlineRef?: boolean;
};

const story: Meta = {
Expand Down Expand Up @@ -88,7 +89,13 @@ const story: Meta = {

export default story;

const Template: Story<Props> = ({ style, className, lazy, ...rest }) => {
const Template: Story<Props> = ({
style,
className,
lazy,
inlineRef,
...rest
}) => {
const { options, error } = useValidateOptions(rest);
const { ref, inView, entry } = useInView(!error ? options : {});
const [isLoading, setIsLoading] = useState(lazy);
Expand All @@ -109,7 +116,11 @@ const Template: Story<Props> = ({ style, className, lazy, ...rest }) => {
return (
<ScrollWrapper indicators={options.initialInView ? 'bottom' : 'all'}>
<Status inView={inView} />
<InViewBlock ref={ref} inView={inView} style={style}>
<InViewBlock
ref={inlineRef ? (node) => ref(node) : ref}
inView={inView}
style={style}
>
<InViewIcon inView={inView} />
<EntryDetails options={options} />
</InViewBlock>
Expand All @@ -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,
Expand Down
64 changes: 41 additions & 23 deletions src/useInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,27 @@ export function useInView({
fallbackInView,
onChange,
}: IntersectionOptions = {}): InViewHookResponse {
const [ref, setRef] = React.useState<Element | null>(null);
const unobserve = React.useRef<Function>();
const callback = React.useRef<IntersectionOptions['onChange']>();
const [state, setState] = React.useState<State>({
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) {
Expand All @@ -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,
Expand All @@ -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;

Expand Down

0 comments on commit 776caa6

Please sign in to comment.