How to debug #use-gesture bumping into something? - reactjs

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.

Related

Multiple MUIDialogs won't always stack in the right order when using Context in React?

I'm currently rendering multiple dialogs on the screen after the user successfully logs in. They're working great...except they won't always stack in the order that I want them too. We have it setup so the dialogs will stack on top of each other. However, it's finicky, and sometimes after a user logs in, they won't be stacked in the desired order.
I'm using React Context for displaying the dialogs globally, and react-router for global state management. I'm currently using Dialog from #material-ui/core, version 4.12.3.
I can't show too much of the code, but here's essentially what's happening in the render method:
//...there is code above
<ThemeProvider theme={theme}>
{dialogs.map(dialog => {
// dialogs defined as a const
const DialogComponent = dialogs[dialog.key].Component
if (!DialogComponent) {
return null
}
return (
<DialogsCurrentContext.Provider key={dialog.id} value={dialog}>
<DialogComponent
params={dialog.props}
data={dialog.data}
DialogProps={{
open: dialog.open,
onClose: onDialogClose,
TransitionProps: {
onExited: () => onDialogExit(dialog.id),
},
}}
/>
</DialogsCurrentContext.Provider>
)
})}
</ThemeProvider>
//...there is code below
Note: In the code, they always appear to render in the right order. We even used sorting, but still will get undesired stacking order. You'll see in the image below.
The Components tab in the React Developer Tools shows the Contexts Providers rendering order. Each one of these Context.Providers container a MUIDialog component:
Components Tab
Here is what actually rendered to the DOM : HTML from Developer Tools
Note: I can't show the dialogs themselves, but I listed which MUIDialog went where with the keys.
I clearly wanted the MUIDialog with key=4 to be on top of key=3, but from what is actually rendered to the DOM, I get key=3 over key=4.
How can this difference happen in the first place? Too me, this looks like a rendering issue, but unsure what.

Rendering issue with custom map component inside tabbed form of react-admin

I am using React-admin for a project where for some resources, I use the tabbed form to better organize the fields and inputs. I created a fairly simple custom map component based on react-leaflet, which I am using in these tabbed forms.
I am facing a weird issue where when the map is on other than the first tab, its contents do not display correctly. It appears as though the map "thinks" the viewport is much smaller than it actually is. Reloading the page (or even just opening developer tools in Chrome) forces re-render of the page and causes the map to start behaving correctly.
To better demonstrate, I created this simple Codesandbox project. It has a user resource from the RA tutorial with two tabs. Both contain an instance of the map component, but while the map on the first tab works correctly right away, the one on the second tab renders incorrectly.
I confess I am still kind of a noob at these things so I may well be doing something wrong, but I've been scratching my head for quite a few hours over this and I'd like to start eliminating possible culprits.
Any insight would be most welcome.
Thanks very much in advance for your time.
This issue has been discussed a lot in SO. If you search a bit you can find the reason. What you actually need to do is two things:
use setInterval in combination with map's invalidateSize method when switching a tab and then clean it on component unmount
use useEffect to change mapZoom and view since they are immutable.
SO since you use react-leaflet version 2.7.x you need to take the map instance using a ref:
const mapRef = useRef();
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current.leafletElement;
const mapZoom = zoom || defaultZoom;
let intervalInstance;
if (!center && marker) {
map.setView(marker, mapZoom);
intervalInstance = setInterval(() => map.invalidateSize(), 100);
} else if (!center) {
map.setView([0.0, 0.0], mapZoom);
}
map.setZoom(mapZoom);
return () => clearInterval(intervalInstance);
}, []);
<LeafletMap
ref={mapRef}
center={marker}
zoom={defaultZoom}
className={classes.leafletContainer}
>
Demo

Performance issues if MapComponent state is updated

I am not sure if this is an issue of react-leaflet-markercluster, react-leaflet, leaflet, react, or my code.
I have a map with several thousand markers and I am using react-leaflet-markercluster for marker clustering. If I need to update a global state of MapComponent, there is 1-3 seconds delay when this change is reflected.
I created a codesandox with 5000 markers and you can see there 2 use cases with performance issues:
1.) MapComponent is inside react-reflex element, that allows resizing panel and propagates new dimensions (width, height) to MapComponent. If width and height are changed, mapRef.invalidateSize() is called to update map dimensions. Resizing is extremely slow.
2.) If user clicks on Marker, global state selected is updated. It is a list of clicked marker ids. Map calls fitBounds method to focus on clicked marker and also marker icon is changed. There is around 1 second delay.
In my project, if I need to change a MapComponent state, it takes 2-3 seconds in dev mode when changes are reflected and it is just a single rerender of MapComponent and its elements (markers).
I took a look at Chrome performance profile and it seems like most time is spent in internal React methods.
It is possible to fix this by preventing rerendering using memo, which is similar to shouldComponentUpdate, but it makes whole code base too complicated. preferCanvas option doesn't change anything. I am wondering what is a good way to fix these issues.
The main problem I identified in your code is that you re-render the whole set of marker components. If you memoize the generation of those, you achieve a good performance boost; instead of running the .map in JSX, you can store all the components in a const; this way, the .map won't run on every render.
from this
...
<MarkerClusterGroup>
{markers.map((marker, i) => {
...
to something like this
const markerComponents = React.useMemo(() => {
return markers.map((marker) => {
return (
<MarkerContainer .../>
);
});
}, [markers, onMarkerClick]);
return (
<>
<MarkerClusterGroup>{markerComponents}</MarkerClusterGroup>
</>
);
The second refactor I tried is changing the way you select a marker. Instead of determining the selected prop from the selected array for each marker, I put a selected field on every marker object and update it when selecting a marker. Also, I add the position to the onClickHandler args to avoid looking for that in the markers array.
There are some other tweaks I don't explain here so please check my codesandbox version.
https://codesandbox.io/s/dreamy-andras-tfl67?file=/src/App.js

React hooks: am I doing an anti pattern? updating state in function outside the component

I am beginning to use React (hooks only), and facing a strange issue. I am trying to reproduce the problem in a small test code, but can't get it to happen, except in my full blown app. This leads me to wonder if I'm doing something really wrong.
I have an array of objects, declared as a state. I map this array to display its content. Except that nothing gets displayed (the array is filled, but nothing gets displayed). Now if I declare an un-related state, make it a boolean which flips each time my array gets updated, then my array gets displayed properly. As if, in the render phase itself, React did not detect the array's changes.
A few things:
the array gets updated by a socketIO connection, I simulate it here with a timer
I update my array OUTSIDE of my component function, BUT providing the setter function to the update function
I also create part of the render fields outside my component function (this has no effect, just for readability in my full app)
I essence, this is what I am doing:
const updateArray = (setTestArray, setTestTag, addArray) => {
setTestArray(prevTestArray => {
let newTestArray = prevTestArray.map((data, index) => (data + addArray[index]))
return newTestArray
})
setTestTag(prevTag => {
return (!prevTag)
})
}
const renderArray = (currentTestArray) => {
return currentTestArray.map((data, index) => (
<div>
testArray[{index}]={data}
</div>
))
}
function TestPage(props) {
const [testArray, setTestArray] = useState([])
const [testTag, setTestTag] = useState(false)
useEffect(() => {
let samples = 3
let initArray= []
for (let i=0; i<samples;i++) initArray[i] = Math.random()
setTestArray(initArray)
// In real code: setup socket here...
setInterval(() => {
let addArray= []
for (let i=0; i<samples;i++) addArray[i] = Math.random()
updateArray(setTestArray, setTestTag, addArray)
}, 1000)
return (() => {
// In real code, disconnect socket here...
})
}, []);
return (
<Paper>
Array content:
{renderArray(testArray)}
<br/>
Tag: {(testTag)? 'true' : 'false'}
</Paper>
)
}
This works just fine. But, in my full app, if I comment out everything concerning "testTag", then my array content never displays. testArray's content is as expected, updates just fine, but placing a debugger inside the map section show that array as empty.
Thus my questions:
is my updateArray function a bad idea? From what I read, my prevTestArray input will always reflect the latest state value, and setTestArray is never supposed to change... This is the only way I see to handle the async calls my socket connection generate, without placing "testArray" in my useEffect dependencies (thus avoiding continuously connecting/disconnecting the socket?)
rendering outside the component, in renderArray, doesn't affect my tests (same result if I move the code inside my component), but is there a problem with this?
As a side note, my array's content is actually more complex is the real app (array of objects), I have tried placing this in this test code, it works just fine too...
Thank you!
Edit: Note that moving updateArray inside the useEffect seems to be the recommended pattern. I did that in my full app. The hook linter does not complain about any missing dependency, yet this still doesn't work in my full app.
But the question is still whether what I am doing here is wrong or not: I know it goes against the guidelines as it prevents the linter from doing its job, but it looks to me like this will still work, the previous state being accessible by default in the setter functions.
Edit #2: Shame on me... silly mistake in my real app code, the equivalent of updateArray had a shallow array copy at some place instead of a deep copy.
Why adding the flipping tag made it work is beyond me (knowing the data was then indeed properly displayed and updated), but getting rid of this mistake solved it all.
I will leave this question on, as the question still stand: is placing the state update, and part of the rendering outside the component a functional problem, or just something which might mater on hide dependencies (preventing the react hooks linter from doing its job) and thus simply bad practice?
The fact is that things work just fine now with both functions outside the component function, which makes sense based on what I understand from hooks at this point.

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;
});
};

Resources