I am trying to create a very basic loading animation inside a react component that displays while my API is loading. I found a solution that does what I want, but once my API is loaded and the component is no longer displaying, I start getting this error:
"Uncaught TypeError: Cannot read properties of null (reading 'innerHTML')"
I have read something about clearing the interval but couldn't figure it out.
Here is the component I built:
const LoadingData = () =>{
var dots = window.setInterval(function() {
var wait = document.getElementById("wait");
if ( wait.innerHTML.length > 5 )
wait.innerHTML = "";
else
wait.innerHTML += ".";
}, 200);
return(
<p>Loading<span id="wait">.</span></p>
)
}
I also get a warning the "dots" is declared, but not used. I am not as concerned about that, but if someone could help me find a better solution I would love to hear it.
Correct way of doing it:
const LoadingData = () => {
// use state to control the dom
const [dots, addDot] = useReducer((state) => (state + 1) % 6, 1);
// set your intervals inside useEffect
useEffect(() => {
const interval = window.setInterval(() => {
addDot();
}, 200);
// remember to clear intervals
return () => clearInterval(interval);
}, [addDot]);
return (
<p>
Loading<span id="wait">{".".repeat(dots)}</span>
</p>
);
};
live example
I have a react DND item on a 'board' which triggers a page scroll if you pull it far up/left/right/down from its original position.
The scrolling during hover is working well, but I have an issue where when I drop the item, it only drops relative to the cursor's position on the screen. It does not account for the additional movement from the auto scrolling. (e.g. if you pull it to the right of the screen and scroll all the way to the right of the board, when you drop the item it will only drop a few hundred pixels to the right, as if no scrolling had occurred).
I have tried to cater for this by adding a window scroll event listener and adding the distance scrolled onto the drop position. BUT, the state in the DOM is using the old state from when the item was picked up and not adding any of the scrolling to the final coordinates of the page.
Am I going about this all wrong?
I have tried some packages like react-dnd-scrollzone, but it looks like they are no longer supported with newer versions of react.
Can anyone provide some insight on how you can account for window scrolling with react DND drop events?
The code I have below ALMOST works, except the item drop only accepts the x and y adjustments the next time AFTER a drop event has been completed, it cannot take the updated state on item drop (it is working on the old state), so the next time you drop an item it moves to the correct position factoring in the scrolling from your last drop. It is always one item drop behind the current state because x and y adjustment state isn't pulled into the DOM until after a drop event is complete....
Here is my code for an item drop event and for page scrolling and the scroll window event listener.
DND Item Drop and Hover Event. The originating scroll coordinates are set on the first instance of 'hover'
const [dragStarted, setDragStarted] = useState(false);
const [origXScroll, setOrigXScroll] = useState(0);
const [origYScroll, setOrigYScroll] = useState(0);
const [xAdjustment, setXAdjustment] = useState(0);
const [yAdjustment, setYAdjustment] = useState(0);
const updatePageState = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id === page._id ? droppedPage : page);
changedPagesCallback(updatedPages);
setTreePages(updatedPages);
}, [pages]);
const [{}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
hover(page, monitor) {
if(!dragStarted) {
setDragStarted(true)
setOrigXScroll(xAdjustment)
setOrigYScroll(yAdjustment)};
const clientOffset = monitor.getClientOffset();
const origPosition = monitor.getInitialClientOffset();
const origX = origPosition.x;
const origY = origPosition.y;
const hoverY = clientOffset.y;
const hoverX = clientOffset.x;
checkPageScroll(origX, origY, hoverX, hoverY);
},
drop(page, monitor) {
setDragStarted(false);
const delta = monitor.getDifferenceFromInitialOffset();
//The issue lies in that difference from initial offset only gives mouse position on the screen and doesn't account for any auto scrolling on the page...
const xAdjuster = xAdjustment < origXScroll ? (xAdjustment - origXScroll) : xAdjustment;
const yAdjuster = yAdjustment < origYScroll ? (yAdjustment - origYScroll) : yAdjustment;
let x = Math.round(page.x + (delta.x)*(1/pageZoomFactor) + xAdjuster);
let y = Math.round(page.y + (delta.y) + yAdjuster);
page = Object.assign(page, {x: x, y: y})
saveUpdatedPage({x: x, y:y}, page._id);
updatePageState(page);
setOrigXScroll(0);
setOrigYScroll(0);
setXAdjustment(0);
setYAdjustment(0);
return undefined;
},
}), [updatePageState]);
Auto Scroller if you pull item far to any side
function checkPageScroll(origX, origY, hoverX, hoverY){
const xScroll = (hoverX - origX)/10;
const yScroll = (hoverY - origY)/10;
const allowedXScroll = xScroll > 8 || xScroll < -8 ? xScroll : 0;
const allowedYScroll = yScroll > 8 || yScroll < -8 ? yScroll : 0;
window.scrollBy(allowedXScroll, allowedYScroll);
}
Window Event Listener to Set the Amount you Have Scrolled. At the start of drag the scroll coordinates are locked as origXScroll and origYScroll, the idea is to add/subtract distance travelled by scrolling left/right up/down on the drop event
useEffect(() => {
window.addEventListener('scroll', () => {
const yScroll = window.scrollY;
const xScroll = window.scrollX;
setXAdjustment(xScroll);
setYAdjustment(yScroll);
})
}, [])
OK, after a mountain of digging I came across the following forum posts in the react dnd issues and discovered a few more tools to get this done. https://github.com/react-dnd/react-dnd/issues/151
Instead of measuring the amount the page has scrolled and adding that to the final coordinates, a better method is to use getInitialSourceClientOffset() and getSourceClientOffset() (both of which I had no idea existed....) to compare the items start and end location.
To do this you will also need a referenced element to compare your items position (can just be a referenced div).
This method still needs a bit of tweaking to account for page zoom, but it is the best I've seen so far.
I'm still working on this, but here's a simplified version of the code for dropping new pages onto the board to see how it could potentially all be put together. I'd recommend using the solutions on the react DND issues link above and reading their posts to get a good idea on how the professionals manage this problem.
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { useDrop } from "react-dnd";
import { ItemTypes } from "../Utils/items";
function PageTree({ pages }) {
const containerRef = useRef("pageBoard");
const [dragStarted, setDragStarted] = useState(false);
//this function to update the state of a page card when a change is made to its position or a widget added
const updatePageState = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id === page._id ? droppedPage : page);
changedPagesCallback(updatedPages);
setTreePages(updatedPages);
}, [pages]);
const [{updateY, updateX}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
hover(page, monitor) {
if(!dragStarted) {
setDragStarted(true)
setOrigXScroll(xAdjustment)
setOrigYScroll(yAdjustment)
};
const clientOffset = monitor.getClientOffset();
const origPosition = monitor.getInitialClientOffset();
const origX = origPosition.x;
const origY = origPosition.y;
const hoverY = clientOffset.y;
const hoverX = clientOffset.x;
checkPageScroll(origX, origY, hoverX, hoverY);
},
drop(page, monitor) {
setDragStarted(false);
const initialPosition = monitor.getInitialSourceClientOffset();
const finalPosition = monitor.getSourceClientOffset();
const container = containerRef.current.getBoundingClientRect();
const newXY = getCorrectDroppedOffsetValue(initialPosition,finalPosition,container);
page = Object.assign(page, {x: newXY.x, y: newXY.y})
saveUpdatedPage({x: newXY.x, y:newXY.y}, page._id);
updatePageState(page);
return undefined;
},
}), [updatePageState]);
useEffect(() => {
OriginalScrollFunc.current = scrollTo
}, [pages]);
function checkPageScroll(origX, origY, hoverX, hoverY){
const xScroll = (hoverX - origX)/10;
const yScroll = (hoverY - origY)/10;
const allowedXScroll = xScroll > 8 || xScroll < -8 ? xScroll : 0;
const allowedYScroll = yScroll > 8 || yScroll < -8 ? yScroll : 0;
window.scrollBy(allowedXScroll, allowedYScroll);
}
function getCorrectDroppedOffsetValue (initialPosition, finalPosition, dropTargetPosition) {
// get the container (view port) position by react ref...
//const dropTargetPosition = ref.current.getBoundingClientRect();
const { y: finalY, x: finalX } = finalPosition;
const { y: initialY, x: initialX } = initialPosition;
// calculate the correct position removing the viewport position.
// finalY > initialY, I'm dragging down, otherwise, dragging up
const newYposition =
finalY > initialY
? initialY + (finalY - initialY) - dropTargetPosition.top
: initialY - (initialY - finalY) - dropTargetPosition.top;
const newXposition =
finalX > initialX
? initialX + (finalX - initialX) - dropTargetPosition.left
: initialX - (initialX - finalX) - dropTargetPosition.left;
return {
x: newXposition,
y: newYposition,
};
};
}
return (
<div ref={containerRef} style={{postion:'relative'}}>
<div ref={drop} style={styles}>
{items.map = > things you drop around}
</div>
</div>
)
}
export default PageTree;
I have a text log component that I want to scroll to the bottom whenever it receives a new log entry, so I have it set the parent's scrollTop to it's scrollHeight in useEffect(). However, this has no effect:
const Log = ({ entries }: LogProps) => {
const logRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (logRef && logRef.current) {
let parent = logRef.current.parentNode as HTMLDivElement
parent.scrollTop = parent.scrollHeight;
}
})
return (
<Module className="log" title="Text Log">
<div ref={logRef} className="log__entries">
...
</div>
</Module>
)
}
This code on the other hand, works:
...
useEffect(() => {
if (logRef && logRef.current) {
let parent = logRef.current.parentNode as HTMLDivElement
setTimeout(() => {
parent.scrollTop = parent.scrollHeight;
}, 100)
}
})
...
On my machine, I can set the timeout as low as 39, and it'll work consistently. Lower numbers have sporadic success. Presumably that number will vary by some performance metric, but I have no idea.
Console logging shows that the ref exists, and it does have height enough to scroll by the time useEffect() triggers. Logging before and after parent.scrollTop = parent.scrollHeight; reveals that scrollTop doesn't change.
Am I misunderstanding how useEffect() works, is it because I'm setting the parent's scrollTop, or is there something else I'm missing?
You can achieve it by making a few changes:
useEffect(() => {
logRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' })
}, [entries])
You can see list of browsers supporting scrollIntoView feature at Mozilla Docs.
If you want more browsers to support then you can install this tiny npm package called smoothscroll-polyfill.
So I've been trying out React Hooks for a change, but there's something I haven't been able to understand, and it's using multiple states in a functional component and interacting between them. Say I want to get the percentage of the window's scroll position and its height, & then display it like so:
import React, { useState, useLayoutEffect } from 'react'
const Page = () => {
const [scrollPos, setScrollPos] = useState(window.pageYOffset || document.documentElement.scrollTop)
const [windowSize, setWindowSize] = useState(window.innerHeight)
const [percent, setPercent] = useState(0)
useLayoutEffect(() => {
const handleScroll = () => {
const y = window.pageYOffset || document.documentElement.scrollTop
setScrollPos(y)
// windowSize is always its initial value, not its latest
console.log("handleScroll: " + y + " / " + windowSize + " = " + y / windowSize)
setPercent(y / windowSize)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
useLayoutEffect(() => {
const handleResize = () => {
const height = window.innerHeight
setWindowSize(height)
// scrollPos is always its initial value, not its latest
console.log("handleScroll: " + scrollPos + " / " + height + " = " + scrollPos / height)
setPercent(scrollPos / windowSize)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return (
<div>
{/* Calculates percentage correctly upon scrolling and resizing */}
<p>{scrollPos} / {windowSize} = {scrollPos / windowSize}</p>
{/* Incorrect once I scroll after resizing and vice versa */}
<p>!= {percent}</p>
</div>
)
}
export default Page
If I run this, the console won't display the latest values of scrollPos and windowSize, but instead the initial ones; while rendering them does show their latest values. And percent gets mixed up with that as it grabs the initial value of one of them upon resizing or scrolling the window.
I think this is one of those things that's caused because of it being asynchronous, but it'd be nice to get a clearer answer on this. How would one be able to work with multiple "local" states, or is it just better to make one single merged state for cases like these?
I'm trying to visualise 500+ vehicles using leaflet. When the position of a marker (vehicle) changes, it will move slowly to reach the destination (using requestAnimationFrame and leaflet's 'native' setLatLng since I don't want to update the state directly). It works well, but I also have a click listener on each marker and notice that it never fires. I soon realised that leaflet has been updating the marker continuously (the DOM element keeps blinking in the inspector). I attempted to log something to see if the component actually re-renders, but it doesn't. Seems like leaflet is messing with the DOM element under the hood.
const Marker = React.memo(function Marker({ plate, coors, prevCoors }) {
const markerRef = React.useRef();
const [activeVehicle, handleActiveVehicleUpdate] = useActiveVehicle();
const heading = prevCoors != null ? GeoHelpers.computeHeading(prevCoors, coors) : 0;
React.useEffect(() => {
if (prevCoors == null) return;
const [prevLat, prevLong] = prevCoors;
const [lat, long] = coors;
let animationStartTime;
const animateMarker = timestamp => {
if (animationStartTime == null) animationStartTime = timestamp;
const progress = (timestamp - animationStartTime) / 5000;
if (progress > 1) return;
const currLat = prevLat + (lat - prevLat) * progress;
const currLong = prevLong + (long - prevLong) * progress;
const position = new LatLng(currLat, currLong);
markerRef.current.leafletElement.setLatLng(position);
requestAnimationFrame(animateMarker);
};
const animationFrame = requestAnimationFrame(animateMarker);
// eslint-disable-next-line consistent-return
return () => cancelAnimationFrame(animationFrame);
}, [coors, prevCoors]);
React.useEffect(() => {
if (plate === '60C23403') console.log('re-render!');
// eslint-disable-next-line
});
return (
<LeafletMarker
icon={createIcon(plate === activeVehicle, heading)}
position={prevCoors != null ? prevCoors : coors}
onClick={handleActiveVehicleUpdate(plate, coors)}
ref={markerRef}
>
<Tooltip>{plate}</Tooltip>
</LeafletMarker>
);
});
How do I prevent this behaviour from leaflet? Any idea is appreciated. Thanks in advance :)