Problem with detecting scroll position in react - reactjs

I'm making a effect similar to FullPage.js (like scroll-snap css)
For that, I defined a state for scroll position
and when scrolling down, I made it move to component automatically
using window.scrollTo.
const Page = () => {
const [pos, setPos] = useState(0);
const target = useRef();
const [snap,setSnap] = useState(false);
const onScroll = () => {
setPos(window.scrollY);
};
useEffect(() => {
window.addEventListener('scroll',onScroll);
return () => {
window.removeEventListener("scroll",onScroll);
}
},[]);
...
if (snap) { // if snap turns true then move to component
target.current.scrollIntoView({behavior:'smooth'})
}
return (
<div>
<Component ref={traget}>
..
</Component>
</div>
)
}
export default Page
This code works. But it moves too slowly.
I think defining a state for scroll position
call re-render. might not be that..
Could you tell me how can I solve this?

Related

Return a value from state only when callback is triggered in React

I am using the react-resizable library to resize columns in a table.
Using the library's API, I can call onSortStart and onSortStop when resizing of the element has started and finished.
When I log in side the triggered callbacks I only see a log once when resizing has started and once when resizing has stopped - so far so good.
But what I want is to be able to return a boolean flag, true when resizing has started and false when resizing has stopped.
The problem is is that the state is changing every time my useStateful custom hook is re-created / rendering. So I get hundreds of updated versions of the variable isResizing.
How can I just make isResizing return true once when started and false once when stopped?
Here is my code:
const ResizableColumn = (props: ResizableColumnProps): ReactElement => {
const { onResizeStart, onResizeStop } = useStateful();
const {
onResize, width, className, children, style,
} = props;
if (!width) {
return <th className={className} style={style}>{children}</th>;
}
return (
<Resizable
width={width}
height={0}
onResize={onResize}
minConstraints={[150, 150]}
draggableOpts={{ enableUserSelectHack: false }}
onResizeStart={onResizeStart}
onResizeStop={onResizeStop}
>
<th className={className} style={style}>{children}</th>
</Resizable>
);
};
export default ResizableColumn;
useStateful:
const useStateful = (): any => {
const [isResizing, setIsResizing] = useState(false);
const onResizeStart = (): void => {
console.log('%c Resizing Started!', 'color: green;'); // logs once - perfect
setIsResizing(true);
};
const onResizeStop = (): void => {
console.log('%c Resizing Stopped!', 'color: green;'); // logs once - perfect
setIsResizing(false);
};
console.log('%c isResizing', 'color: green;', isResizing); // logs hundreds of times with different values
return { isResizing, onResizeStart, onResizeStop };
};
export default useStateful;
I have also tried using useRef but the same thing happens:
const useStateful = (): any => {
const isResizing = useRef<boolean>();
const onResizeStart = (): void => {
console.log('%c Resizing Started!', 'color: green;'); // logs once - perfect
isResizing.current = true;
};
const onResizeStop = (): void => {
console.log('%c Resizing Stopped!', 'color: green;'); // logs once - perfect
isResizing.current = false;
};
console.log('%c isResizing', 'color: green;', isResizing.current); // logs hundreds of times with different values
return { isResizing, onResizeStart, onResizeStop };
};
export default useStateful;
Ciao, I saw your code for a lot of time and the only thing I noted is that on your custom hook useStateful you are returning a different number of elements compared to the line in which you are using your custom hook. I made a small example here.
The custom hook is conceptually equal to yours:
const useStateful = () => {
const [isResizing, setIsResizing] = useState(false);
const onResizeStart = () => {
setIsResizing(true);
console.log("onResizeStart");
};
const onResizeStop = () => {
setIsResizing(false);
console.log("onResizeStop");
};
console.log(isResizing);
return [isResizing, onResizeStart, onResizeStop];
};
But, when I use this hook I wrote:
const [isResizing, onResizeStart, onResizeStop] = useStateful();
Thats it. I konw that my example could not be what you are looking for but it's a little bit tricky to recreate your environment.

Hide and Show modal on mouseenter and mouseleave using React Hooks

I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.

React custom Hook using useRef returns null for the first time the calling component Loads?

I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}

Are all variables inside a useEffect absolutely required to be listed as dependencies?

Do all variables used inside a useEffect always, absolutely, without exception need to be specified as dependencies?
My use case (simplified for demonstration purposes) involves different functions being executed depending on the width of the browser window, but not run when browser window changes:
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
useEffect(() => {
isDesktop ? scrollToTop() : scrollToTopOfArticle()
}, [selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
);
}
If I add isDesktop to the dependencies then the effect runs whenever the user resizes the window between mobile and desktop, which is not desired, but I'm also aware of the dogma that everything used inside the effect must be listed as a dependency.
Any suggestions on how to reconcile these two requirements?
If you want to make a useEffect() only responsive to a change in selectedArticle, use isDesktop and selectedArticle to initialize component states. Whenever selectedArticle changes, the first useEffect() will update both states with the passed-in props, and trigger the second useEffect() to re-run on the next render.
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const [desktop, setDesktop] = useState(isDesktop)
const [article, setArticle] = useState(selectedArticle)
useEffect(() => {
if (article !== selectedArticle) {
setDesktop(isDesktop)
setArticle(selectedArticle)
}
}, [isDesktop, selectedArticle, article])
useEffect(() => {
if (desktop) scrollToTop()
else scrollToTopOfArticle()
}, [desktop, article])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Alternatively, you can abstract this latching behavior to another hook so that isDesktop only updates to its live value when selectedArticle changes. Note that selectedArticle still needs to be a dependency of the scroll action effect, so that the useEffect() will trigger the scroll action on every change to selectedArticle even if isDesktop has not changed values since the last trigger.
const useLatch = (value, deps) => {
const [state, setState] = useState(value)
const effect = useCallback(() => { setState(value) }, [value])
useEffect(effect, deps)
return state
}
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const latchedIsDesktop = useLatch(isDesktop, [selectedArticle])
useEffect(() => {
if (latchedIsDesktop) scrollToTop()
else scrollToTopOfArticle()
}, [latchedIsDesktop, selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Are all variables inside a useEffect absolutely required to be listed as dependencies?
Yes or else it will generate a warning.
That's why it's best to have clear idea what each effect should do (i.e separation of concern).
Thus, you can have two separate effects with different dependencies.
Something like:
useEffect(
() => {
scrollToTopOfArticle();
}, [selectedArticle]
);
useEffect(
() => {
if (selectedArticle && isDesktop) {
scrollToTop();
}
}, [isDesktop, selectedArticle]
)

React Hooks - Removing component after use

I'm trying to wrap my head around this. My custom hook is supposed to create a simple popup with the desired input and remove after 3 seconds. Of course, currently, it re-renders every time the counter has reset. How can I make it render only once and then be removed from the dom?
export function createPopup(content, popupType) {
const [message, setMessage] = useState(content)
const [type, setType] = useState(popupType)
const [value, setCounter] = useState(3)
const myTimer = setTimeout(() => {
return setCounter(value - 1)
}, 1000)
useLayoutEffect(() => {
const id = setTimeout(() => {
setCounter(value + -1);
}, 1000);
return () => {
clearTimeout(id);
};
}, [value])
return (
<>
{value !== 0 &&
<PopupMolecule type={type}>
{message}
</PopupMolecule>
}
</>
)
}
I think you want something more like this:
export function createPopup(content, popupType) {
const [visible, setVisible] = useState(true);
useEffect(() => {
setTimeout(() => setVisible(false), 3000);
}, []);
return (
<>
{visible &&
<PopupMolecule type={popupType}>
{content}
</PopupMolecule>
}
</>
)
}
There are still some improvements that need to made here (i.e. fading out on exit or some transition, and the way this is setup you can't use it more than once), but this should fix the problem you stated.
This will show your popup for three seconds on mount, then make your popup invisible and unmount it from the DOM. The empty array in the useEffect hook lets it know to only trigger on mount (and unmount if you return a value). You also don't need the other state variables that you're not updating. Those can just be passed in as function parameters.

Resources