Reputation: 329
I try to implement scroll indicator in component using custom hook.
Here's the component: ...
const DetailListInfo: React.FC<Props> = props => {
const container = useRef(null)
const scrollable = useScroll(container.current)
const { details } = props
return (
<div
ref={container}
className="content-list-info content-list-info-detailed"
>
{details && renderTypeDetails(details)}
{scrollable && <ScrollIndicator />}
</div>
)
}
export default inject("store")(observer(DetailListInfo))
And the useScroll hook:
import React, { useState, useEffect } from "react"
import { checkIfScrollable } from "../utils/scrollableElement"
export const useScroll = (container: HTMLElement) => {
const [isScrollNeeded, setScrollValue] = useState(true)
const [isScrollable, setScrollable] = useState(false)
const checkScrollPosition = (): void => {
const scrollDiv = container
const result =
scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
scrollDiv.scrollTop === 0
setScrollValue(result)
}
useEffect(() => {
console.log("Hook called")
if (!container) return null
container.addEventListener("scroll", checkScrollPosition)
setScrollable(checkIfScrollable(container))
return () => container.removeEventListener("scroll", checkScrollPosition)
}, [isScrollable, isScrollNeeded])
return isScrollNeeded && isScrollable
}
So on every scroll in this passed component (containers are different, that's why I want to make customizable hook) I want to check for current scroll position to conditionally show or hide the indicator. The problem is, that hook is called only once, when component is rendered. It's not listening on scroll events. When this hooks was inside the component, it was working fine. What is wrong here?
Upvotes: 1
Views: 2005
Reputation: 329
Hook that has a scroll listener inside:
export const ScrollIndicator: React.FC<Props> = props => {
const { container } = props
const [isScrollNeeded, setScrollValue] = useState(true)
const [isScrollable, setScrollable] = useState(false)
const handleScroll = (): void => {
const scrollDiv = container
const result =
scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
scrollDiv.scrollTop === 0
setScrollValue(result)
}
useEffect(() => {
setScrollable(checkIfScrollable(container))
container.addEventListener("scroll", handleScroll)
return () => container.removeEventListener("scroll", handleScroll)
}, [container, handleScroll])
return isScrollable && isScrollNeeded && <Indicator />
}
In the render component there's need to check if the ref container exists. This will call hook only if container is already in DOM.
const scrollDiv = useRef(null)
{scrollDiv.current && <ScrollIndicator container={scrollDiv.current} />}
Upvotes: 0
Reputation: 473
Let's research your code:
const container = useRef(null)
const scrollable = useScroll(container.current) // initial container.current is null
// useScroll
const useScroll = (container: HTMLElement) => {
// container === null at the first render
...
// useEffect depends only from isScrollable, isScrollNeeded
// these variables are changed inside the scroll listener and this hook body
// but at the first render the container is null so the scroll subscription is not initiated
// and hook body won't be executed fully because there's return statement
useEffect(() => {
if (!container) return null
...
}, [isScrollable, isScrollNeeded])
}
To make everything work correctly, your useEffect
hooks should have all dependencies used inside the hook body. Pay attention to the warning notes in the documentation.
Also you can't pass only ref.current
into a hook. This field is mutable and your hook will not be notified (re-executed) when ref.current
is changed (when it's mounted). You should pass whole ref
object to be able to get HTML Element by ref.current
inside the useEffect
.
The correct version of this function should look like:
export const useScroll = (ref: React.RefObject<HTMLElement>) => {
const [isScrollNeeded, setScrollValue] = useState(true);
const [isScrollable, setScrollable] = useState(false);
useEffect(() => {
const container = ref.current;
if (!container) return;
const checkScrollPosition = (): void => {
const scrollDiv = container;
const result =
scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
scrollDiv.scrollTop === 0;
setScrollValue(result);
setScrollable(checkIfScrollable(scrollDiv));
};
container.addEventListener("scroll", checkScrollPosition);
setScrollable(checkIfScrollable(container));
return () => container.removeEventListener("scroll", checkScrollPosition);
// this is not the best place to depend on isScrollNeeded or isScrollable
// because every time on these variables are changed scroll subscription will be reinitialized
// it seems that it is better to do all calculations inside the scroll handler
}, [ref]);
return isScrollNeeded && isScrollable
}
// somewhere in a render:
const ref = useRef(null);
const isScrollable = useScroll(ref);
Upvotes: 2