implmenting Scorll postion in reactwithout rerending the whole component? - reactjs

I want to be able to listen to scroll event to get the current value, and if the value reaches a certain threshold render a div the current logic works well with useState but it is rendering every render.
useRef however doesn't seem do be doing what is should, is there any solution to this ? will callback solve this ? if possible could you refactor to a better logic.
const scrollRef = useRef<number>(0);
useEffect(() => {
const listenToScroll = () => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = winScroll / height;
scrollRef.current = scrolled;
};
const fn = window.addEventListener('scroll', listenToScroll);
return fn;
}, []);

Related

window-based react hook makes sidebar flicker

I'm using this react hook in a next.js app.
It is supposed to return the width and the state of my sidebar
export const useSidebarWidth = () => {
const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_WIDTH);
const handleResize = () => {
if (window.innerWidth < SIDEBAR_BREAKPOINT) {
setSidebarWidth(SIDEBAR_WIDTH);
} else {
setSidebarWidth(SIDEBAR_WIDTH_EXPANDED);
}
};
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const isExpanded = useMemo(() => {
return sidebarWidth === SIDEBAR_WIDTH_EXPANDED;
}, [sidebarWidth]);
return {
sidebarWidth: isExpanded ? SIDEBAR_WIDTH_EXPANDED : SIDEBAR_WIDTH,
isExpanded,
};
};
Unfortunately, when navigating through my app, the sidebar flickers: for a short moment, it takes its small width, and immediately after, it expands. This happens every time I move to another page.
How can I make sure that this doesn't happen ?
Depending on your build configuration, window.innerwidth may be initially set to 0 for the first frame and then be updated next render. For example, I tried re-creating this issue and got the same result in a CRA application but not in a Vite application.
Either way, one possible solution is to use window.outerWidth, which is usually the same as window.innerWidth for most end-users. A possible implementation in your case would be:
const width = window.innerWidth === 0 ? window.outerWidth : window.innerWidth;
if (width < SIDEBAR_BREAKPOINT) {
setSidebarWidth(SIDEBAR_WIDTH);
} else {
setSidebarWidth(SIDEBAR_WIDTH_EXPANDED);
}
This is obviously not a perfect solution, but I think it's better than what you have, as window.outerWidth is a more accurate estimate of window.innerWidth than 0 is.

Performant way of getting mouse position every frame for canvas animation in React?

I have an animated background using canvas and requestAnimationFrame in my React app and I am trying to have its moving particles interact with the mouse pointer, but every solution I try ranges from significantly slowing down the animation the moment I start moving the mouse to pretty much crashing the browser.
The structure of the animated background component goes something like this:
<BackgroundParentComponent> // Gets mounted only once
<Canvas> // Reutilizable canvas, updates every frame.
// v - dozens of moving particles (just canvas drawing logic, no JSX return).
// Each particle calculates its next frame updated state every frame.
{particlesArray.map(particle => <Particle/>}
<Canvas/>
<BackgroundParentComponent />
I have tried moving the event listeners to every level of the component structure, calling them with a custom hook with an useRef to hold the value without rerendering, throttling the mouse event listener so that it does not fire that often... nothing seems to help. This is my custom hook right now:
const useMousePosition = () => {
const mousePosition = useRef({ x: null, y: null });
useEffect(() => {
window.addEventListener('mousemove', throttle(200, (event) => {
mousePosition.current = { x: event.x, y: event.y };
}))
});
useEffect(() => {
window.addEventListener('mouseout', throttle(500, () => {
mousePosition.current = { x: null, y: null };
}));
});
return mousePosition.current;
}
const throttle = (delay: number, fn: (...args: any[]) => void) => {
let shouldWait = false;
return (...args: any[]) => {
if (shouldWait) return;
fn(...args);
shouldWait = true;
setTimeout(() => shouldWait = false, delay);
return;
// return fn(...args);
}
}
For reference, my canvas component responsible of the animation looks roughly like this:
const AnimatedCanvas = ({ children, dimensions }) => {
const canvasRef = useRef(null);
const [renderingContext, setRenderingContext] = useState(null);
const [frameCount, setFrameCount] = useState(0);
// Initialize Canvas
useEffect(() => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
canvas.width = dimensions.width;
canvas.height = dimensions.height;
const canvas2DContext = canvas.getContext('2d');
setRenderingContext(canvas2DContext);
}, [dimensions]);
// make component re-render every frame
useEffect(() => {
const frameId = requestAnimationFrame(() => {
setFrameCount(frameCount + 1);
});
return () => {cancelAnimationFrame(frameId)};
}, [frameCount, setFrameCount]);
// clear canvas with each render to erase previous frame
if (renderingContext !== null) {
renderingContext.clearRect(0, 0, dimensions.width, dimensions.height);
}
return (
<Canvas2dContext.Provider value={renderingContext}>
<FrameContext.Provider value={frameCount}>
<canvas ref={canvasRef}>
{children}
</canvas>
</FrameContext.Provider>
</Canvas2dContext.Provider>
);
};
The mapped <Particle/> components are are fed to the above canvas component as children:
const Particle = (props) => {
const canvas = useContext(Canvas2dContext);
useContext(FrameContext); // only here to force the force that the particle re-render each frame after the canvas is cleared.
// lots of state calculating logic here
// This is where I need to know mouse position every (or every few) frames in order to modify each particle's behaviour when near the pointer.
canvas.beginPath();
// canvas drawing logic
return null;
}
Just to clarify, the animation is always moving regardless of the mouse being idle, I've seen other solutions that only work for animations triggered exclusively by mouse movement.
Is there any performant way of accessing the mouse position each frame in the Particle mapped components without choking the browser? Is there a better way of handling this type of interactive animation with React?

React onClick function side affects messing with animation

My function is messing up my animation. I have a little audio player that uses onClick play/pause button to help manage information for a couple useStates (currentTime and duration) to and move the progress bar and update the time ticker while the song is playing and stop when the song is paused. So it runs every second. I also want an animation to start and pause onClick of the same button. Problem is, every time it runs, it resets the animation so it . Any help would be greatly appreciated. Thanks, ya'll.
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [animation, setAnimation] = useState(false);
const isPlayingHandler = () => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
if (!prevValue) {
audio.current.play();
setAnimation(true);
progressBarAnimation.current = requestAnimationFrame(whilePlaying);
} else {
audio.current.pause();
setAnimation(false);
cancelAnimationFrame(progressBarAnimation.current);
};
<button className={styles.playPauseButton} onClick={isPlayingHandler}>
{ isPlaying ? <BsPauseCircleFill /> : <BsPlayCircleFill /> }</button>
There is way more code but these are the bits that affect the animation state.
It's working fine by default the component's animation will be re-rendered. The solution when you want to not to let specific part of the application to re-render ( always ) is to wrap that thing into useCallback function ( it will return the memoized animations which will change iff dependency array of it changes ( same as useEffect )). Here is how your code will look like ( it's just and idea ):
const memoizedCallback = useCallback(
() => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
if (!prevValue) {
audio.current.play();
setAnimation(true);
progressBarAnimation.current =
requestAnimationFrame(whilePlaying);
} else {
audio.current.pause();
setAnimation(false);
cancelAnimationFrame(progressBarAnimation.current);
},
[setAnimation, requestAnimationFrame, animation],
);

How to account for page scrolling with react-DND drop events?

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;

Leafet setLatLng unmounts and mounts markers continuously, preventing events from firing

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 :)

Resources