React useRef scrolling just working within setTimeout - reactjs

I'm trying to use scrollIntoView on a react application, and therefore in a page I am using this
useEffect(() => {
myRef.current.scrollIntoView({ behavior: 'smooth' });
}, [myRef, selectedSectionIndex, modalOpen]);
It works on almost every situation as I have the ref={myRef} associated to the element that I want to put into view. However, in one of the transitions even if the code is called it does nothing (not even throws an error). Only if I change it to
useEffect(() => {
setTimeout(() => myRef.current.scrollIntoView({ behavior: 'smooth' }), 0);
}, [myRef, selectedSectionIndex, modalOpen]);
I have read about similar issues, and that's why I actually tried this solution. But can't really put my finger on how this solves it. I keep reading the referenced dom element might not be rendered yet when the first code runs, but
a. Why would it be on the second one?
b. Putting a debugger just above that line I can see the element is present in the DOM and even more
if I release the debuger, same and silently does nothing but if I step over and run that line it actually scrolls.Edit: I attach a screenshot of the debugger, if I press f8 nothing, if I press f10 it scrolls
I would like to thin there's a cleaner way of making this work but can't find it
Thanks

Related

How to debug #use-gesture bumping into something?

I'm using #use-gesture/react to drag a div around. The dragging is working just fine, but it keeps "bumping into" something. It appears that the motion is constrained by some containing div, but I can't for the life of me figure out why.
There are a few things I can imagine would be useful in diagnosing this, and I don't know how to do any of them :)
is there some event that fires when the component "runs into"
something, such that I can log to console what it's hitting?
is there any way to sort of visually inspect the DOM along the
z-index? (I have tried manually scouring the DOM, and can't see
anything that would cause a problem)
is there some way to watch local variables in the
react-developer-tools chrome extension?
I'm in no way constrained to the above — I'll take any and all advice on how to figure this out. More details, below.
Details
At a high level, I'm trying to add a moderately complicated component that has some draggable sub-components to part of a larger app. So, something like
const App = () => {
...
return (
<BrowserRouter >
... lots of code
<SomeComplicatedThing /> // with some sub-component that uses #use-gesture
...
Everything is very close to working, except for this annoying problem of the drag action appearing to run into some boundary.
After going over both the code and the DOM seemingly countless times and not being able to spot any reason for the problem, I decided to manually reconstruct one page to try and reproduce the problem and narrow it down to some specific component.
You can see that I'm using useDrag with the hook bound to a div. When the event fires, I put a copy of the div in a portal tied to the body, and then update the top and left style attributes inside of a useLayoutEffect block:
function App() {
function useColumnDragState() {
const [dragState, setDragState] = useState(null);
const bindDrag = useDrag(({ first, last, event, xy }) => {
if (first && event) {
event.preventDefault();
setDragState({
... // do state stuff
});
} else if (last) {
setDragState(null);
}
...
}, {});
return {
dragState,
dragEventBinder: bindDrag
};
}
const {
dragState,
dragEventBinder
} = useColumnDragState();
const eventHandlers = useMemo(() => dragEventBinder(), [dragEventBinder]);
const ColumnDragObject = ({ dragState }) => {
const referenceBoxRef = useRef(null);
const dragBoxRef = useRef(null);
const dragObjectPortal = dragState ? createPortal(
<div> // wrapper
<div ref={dragBoxRef}> // object positioner
<div> // object holder
<MyComponent /> //clone
</div>
</div>
</div>,
document.body
)
: null;
// set up initial position
...
// subscribe to live position updates without state changes
useLayoutEffect(() => {
if (dragState) {
dragState.updateListeners['dragObj'] = (xy: number[]) => {
if (!dragBoxRef.current) {
return;
}
dragBoxRef.current.style.left = `${xy[0]}px`;
dragBoxRef.current.style.top = `${xy[1]}px`;
};
}
}, [dragState]);
return <div ref={referenceBoxRef}>{dragObjectPortal}</div>
}
return (
<>
...
... // many layers of nested stuff
...
<div {...eventHandlers}> // draggable
<MyComponent /> // original
...
<ColumnDragObject dragState={dragState}/>
...
</>
);
}
export default App;
As I said, that's a much simplified version of both the component and the app, extracted out into one file. Much to my annoyance, it works just fine (i.e., it doesn't demonstrate the problem).
Next I did what maybe should have been my first step, and pulled <SomeComplicatedThing /> out into a stand-alone app with it at the top level:
import React, { Fragment } from 'react';
import { CssBaseline } from '#mui/material';
import SomeComplicatedThing from './components/SomeComplicatedThing';
function App() {
return (
<>
<CssBaseline enableColorScheme />
<div className="App">
<SomeComplicatedThing />
</div>
</>
);
}
export default App;
As I said, I probably should have done that first, because it does exhibit the problem. So, I opened the two up side-by-side, and stepped through the DOM. As far as I can see in the DOM between the working one (where I can drag stuff all over the page), and the not working one (where the drag appears to be constrained to some enclosing element) there is no difference in the hierarchy, and no difference in display, position, overflow, or z-index for any of the elements in the hierarchy.
So, how do I debug this from here? As I said in the beginning, my initial thoughts were
is there some event that fires when the component "runs into"
something, such that I can log to console what it's hitting?
I realise there's no actual running into things. The actual behaviour is that the pointer can move anywhere on the screen, but the <div> whose top and left attributes are being updated in the above code stops moving beyond a certain point (updates are still happening — if you move the pointer back from the "out of bounds" portion of the screen, the <div> moves again). One thought I have is that maybe some containing element has some attribute set, such that it precludes the xy coordinates from being updated, and there may be some event that fires when that "fail to update" occurs that can tell me which element is doing the blocking.
is there any way to sort of visually inspect the DOM along the
z-index? (I have tried manually scouring the DOM, and can't see
anything that would cause a problem)
I keep reading sort of parenthetically in digging around about this that there can sometimes be z-index issues with react-use-gesture. I had thought going the createPortal route would avoid any of those; nevertheless, it would be good to get some visual 3-D representation of the DOM and make sure I'm not missing something "obvious"
is there some way to watch local variables in the
react-developer-tools chrome extension?
As a last resort, I tried looking at what xy coordinates were being set, thinking that I might see something to make me go "aha!" when it runs into whatever is containing it. However, if I try and set a watch on xy naively in react-developer-tools, it doesn't work. In order to set a watch on it, I need to set a breakpoint in the enclosing function, and then set the watch. The problem is, since we're updating mouse position, every time you move at all, it triggers the break point, but if I remove the break point, it stops watching the variable. So ... how can I get it to dynamically watch a variable local to a function without setting a breakpoint?
And, of course, as I said at the beginning, any other debugging ideas are most definitely welcome!
Background
Given that I have the "simpler" version working, and that the normal course of development is to build-test-build-test-build-test ... until one gets to a finished product, it's natural to wonder how I got to the "complicated and broken" state in the first place. The answer is, my starting point is react-csv-importer. However, there are two aspects of that package which are rather explicitly the opposite of what I want: it's written in TypeScript and the UI theme is standalone. I'm removing the TypeScript parts to make it pure JavaScript, and making the UI be MUI.

Scroll to props.location.hash using useEffect after component mounts

I am creating a glossary page where each term has it's own card that is not expanded by default. Each card uses the term as it's ID because they will all be unique. I want to support direct links to a specific term via URL hash.
So, if the URL is localhost:3000/#/glossary#Actor, the initial load will scroll to the term and 'open' the card by simulating the click on the element (click handler on that element opens the card).
It is working, but too frequently. Every time I enter text into the search bar or cause a render in any way, it re-scrolls and does the card open animation again. I just want to initially scroll to the term if there is a hash, then ignore it unless it changes.
It only works if I don't include a dep array. If I include a dep array with props.location.hash, the el returns as null and nothing happens. I'm aware that the restarting of the effect is happening because of no dep array, but I don't know why it doesn't work when I add it.
useEffect(() => {
//looks for hash in URL; gets element associated with the hash
let el = document.getElementById(decodeURI(props.location.hash.slice(1)));
console.log(el)
//if element exists, get coords and offset for header before scrolling
if (el) {
const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset;
const yOffset = -80;
window.scrollTo({
top: yCoordinate + yOffset,
behavior: 'smooth'
});
//opens the card only if not already open
if (!el.classList.contains('openTermCard')) {
el.click();
}
}
})
useEffect may not be the correct method. I might not even need a hook for this, idk. I just want this initial scroll event to happen on page load and not again.
It's a bit hard to figure out a solution without a working sandbox.
From your description, it seems to me your component is rendering more than once on the first load. If you put [] as dependency array, the effect only runs once, when the element is not defined yet (hence why you get null) and that's why it doesn't work.
One way to go about it is to think what else you can do in terms of designing your app. You mention that every time your component updates it focuses the card again, but that's kind of what you expect when you're using useEffect. Is it possible to re-structure the component so that the parent (the one with this side effect) doesn't re-render as often?
If you can provide a code sandbox I'm sure I (and probably others) will be able to help you further.

Interval not causing re-render in react component

I am using the useEffect react hook to set an interval for a countdown variable. I have noticed that when the tab in my browser isn't active, the internal stops working. If the tab is active the code works exactly as expected.
Is there a better way to do this? Below is an example of the code.
export default function CountdownTimer({ minutes_left, action }) {
const [timeLeft, setTimeLeft] = useState();
function updateTimer() {
const remaining = ...work out time left
setTimeLeft(remaining);
}
useEffect(() => {
const interval = setInterval(() => {
updateTimer();
if (timeLeft.asMilliseconds() <= 0) {
action();
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<p>{timeLeft}</p>
);
}
I have implemented solutions in Dan Abramov's post here about this problem. However Dan's solution's also don't render when the tab is inactive.
Note that this is how browsers work: inactive tabs are deprioritised and timers get heavily throttled. Remember that setTimeout and setInterval are most expressly not "run code once X ms have passed", they are "run code after at least X ms have passed" without any guarantee that things won't actually happen much, much later due to scheduling (e.g. whether the tab is active, or even visible as far as the OS can inform the browser) or even just because a bit of JS code took longer than your indicated interval to complete and so your timeout literally can't fire until it's done (because JS is single-threaded).
For Firefox, See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API#Policies_in_place_to_aid_background_page_performance on what kind of throttling is performed on timers, and for Chrome, see https://developers.google.com/web/updates/2017/03/background_tabs
(For any other browser, a web search should find you similar documentation.)
However, think about what it means for a tab to become a background process: if your users can't see your tab, why would you need to update anything at all? Your users won't be able to see those updates. Instead, consider suspending purely visual tasks when a tab becomes invisible, and then resuming them when the tab becomes visible again, and using a clock-time check to determine whether "things need to happen".
In fact, this is important to do even on a foreground tab if you use setInterval or setTimeout: 10 consecutive setTimeout(..., 100) do not imply that it'll be exactly 1 second later once they're done, it means it's at least 1s later when we're done but we could easily reach that "one second later" mark by the time the 7th or 8th timeout fires because JS is single threaded, and if some task took a while, your timeout won't fire until that task is done. So you're going to at the very least need to update your code for that already anyway.
Probably because useEffect call use clearInterval function after the first render. Here's how the cleanup for useEffect works with the returned function - Example Using Hooks.

React stackable snackbars/toasts

I'm creating my own simple snackbar/toast stacker. However, I'm having problems with queing them in an orderly manner. Removing a snackbar from the snackbar que causes re-render and odd behavior.
The basic flow:
Click a button which causes the addSnack function to fire which is provided by the withSnackbar HOC.
Take the parameters from the fired function, and create a snack accordingly and add it to the snackbar list.
At the end, we render the snackbar list.
Each snackbar controls it's own appearance and disappearance, and is controlled by a time out. After the timeout is fired, it calls removeSnack function which is suppose to remove the first snack from the list.
codesandbox
If you click the button for example, four times in a short amount of time. They render nicely, but when the first one is to be deleted, they all disappear and reappear abnormally.
I understand that it's partially the state re-renderings fault, however, I'm not sure how to handle it in a way that the removal is handled gracefully without affecting the rendering of other snacks.
So, after many hours of trial and error, I found a solution that works so far. Moving and reading the snacks outside of the state helped with the bizarre rendering problems, and with it, I was able to create a message que which works well.
Working example
Codesandbox
If you look at splice document, you will notice that it's returning an array of deleted elements and not the initial array.
You can correct it by splicing then updating:
snacks.splice(-1, 1);
addSnacks(snacks);
However you are still going to have some weird behavior and you might need to use a keyed list to fix that.
i had the same issue and i saw your solution, but i was really trying to find out why it happens - here is why:
when u call a useState hook from an async function's callback, you should use the callback format of the hook to make sure that you are working with the latest value. example:
const [messages, setMessages] = useState([]);
const addMessage = ( message ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
return [ ...prevMessages, message ];
});
};
const removeMessage = ( index ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
let newMessages = [...prevMessages];
newMessages.splice( index, 1 );
return newMessages;
});
};

Trying to trigger a WheelEvent programmatically with Hammerjs

I am using a library (https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu) that scrolls on use of the mousewheel, and I want to use this functionality when swiping left or right.
I am using hammerjs to replicate swipeleft and swiperight behavior, and this is working.
However, creating a WheelEvent does not seem to trigger the functionality dependent on the WheelEvent.
I am using componentDidUpdate for now as my react lifecycle method because for some reason this.containerRef.current is always null in componentDidMount, but once I figure out the reason behind that, I'll probably move it.
Anyway, here's my code:
componentDidUpdate() {
if(this.containerRef.current !== null) {
this.hammer = Hammer(this.containerRef.current)
this.hammer.on('swiperight', () => alert("swipe right"));
var wheelevent = new WheelEvent("wheel", {deltaX: 500, deltaY: 500});
this.hammer.on('swiperight', () => window.dispatchEvent(wheelevent));
}
}
Now I want to point out, the alert for swipe right DOES happen, so the behavior is definitely triggering, however my WheelEvent is not being caught by the scroll library.
How should I trigger a WheelEvent programmatically?
EDIT - I made a codepen about it:
https://codesandbox.io/s/react-horizontal-scrolling-menu-fi7tv
My hunch is that issue is related to Dragging being disabled and the event is canceled.
So you need to send the event down the chain a bit. I have updated the codesandbox below which works
https://codesandbox.io/s/react-horizontal-scrolling-menu-j46l8
The updated code part is below
var elem = document.getElementsByClassName("menu-wrapper")[0];
this.hammer.on("swiperight", () => elem.dispatchEvent(wheeleventRight));
this.hammer.on("swipeleft", () => elem.dispatchEvent(wheeleventLeft));
You may want to better the approach though in a more reactive fashion later. But this does show that once you sent the event in lower order elements the wheeling does work well

Resources