Using array of Refs intead of single Ref - javascript

I'm using ref inside a map loop.
I need an array of refs
The problem is the ref only target the last element generated in the list
here is the example I prepared,
I need to have the custom hook run on all generated elements inside the map loop by a list of ref
I'm looking for a way without introducing another Component
import React, { useRef, useState, useEffect, useCallback } from "react";
/// throttle.ts
export const throttle = (f) => {
let token = null,
lastArgs = null;
const invoke = () => {
f(...lastArgs);
token = null;
};
const result = (...args) => {
lastArgs = args;
if (!token) {
token = requestAnimationFrame(invoke);
}
};
result.cancel = () => token && cancelAnimationFrame(token);
return result;
};
const id = (x) => x;
const useDraggable = ({ onDrag = id } = {}) => {
const [pressed, setPressed] = useState(false);
const position = useRef({ x: 0, y: 0 });
const ref = useRef();
const unsubscribe = useRef();
const legacyRef = useCallback((elem) => {
ref.current = elem;
if (unsubscribe.current) {
unsubscribe.current();
}
if (!elem) {
return;
}
const handleMouseDown = (e) => {
e.target.style.userSelect = "none";
setPressed(true);
};
elem.addEventListener("mousedown", handleMouseDown);
unsubscribe.current = () => {
elem.removeEventListener("mousedown", handleMouseDown);
};
}, []);
useEffect(() => {
if (!pressed) {
return;
}
const handleMouseMove = throttle((event) => {
if (!ref.current || !position.current) {
return;
}
const pos = position.current;
const elem = ref.current;
position.current = onDrag({
x: pos.x + event.movementX,
y: pos.y + event.movementY
});
elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
});
const handleMouseUp = (e) => {
e.target.style.userSelect = "auto";
setPressed(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
handleMouseMove.cancel();
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [pressed, onDrag]);
return [legacyRef, pressed];
};
/// example.ts
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
};
const DraggableComponent = () => {
const handleDrag = useCallback(
({ x, y }) => ({
x: Math.max(0, x),
y: Math.max(0, y)
}),
[]
);
const [ref, pressed] = useDraggable({
onDrag: handleDrag
});
return (
<>
{[1, 2, 3].map((el, i) => (
<div key={"element" + i} ref={ref} style={quickAndDirtyStyle}>
<p>{pressed ? "Dragging..." : "Press to drag"}</p>
</div>
))}
</>
);
};
export default function App() {
return (
<div className="App">
<DraggableComponent />
</div>
);
}
a link to a codesandbox is here
https://codesandbox.io/s/determined-wave-pfklec?file=/src/App.js

Assuming that the goal is to make each generated element draggable individually, here is an example by switching some ref to arrays, and changed pressed to number | boolean to pass an index.
Changed the names of legacyRef and pressed to handleRefs and pressedIndex to reflect the difference in their use case.
Forked live demo on: codesandbox (updated to omit the use of useCallback)
However, with the hook applied, it seems that each element (except the first one) has a limited draggable area.
The posted example also has this behavior on the third draggable item, so not sure if this is intended by the hook. If not, perhaps the implement of draggable need to be adjusted to be fit for all elements.
Hope that this could help as a reference.
import React, { useRef, useState, useEffect } from "react";
/// throttle.ts
export const throttle = (f) => {
let token = null,
lastArgs = null;
const invoke = () => {
f(...lastArgs);
token = null;
};
const result = (...args) => {
lastArgs = args;
if (!token) {
token = requestAnimationFrame(invoke);
}
};
result.cancel = () => token && cancelAnimationFrame(token);
return result;
};
const id = (x) => x;
const useDraggable = ({ onDrag = id } = {}) => {
const [pressedIndex, setPressedIndex] = useState(false);
const positions = useRef([]);
const refs = useRef([]);
const unsubscribes = useRef([]);
const handleRefs = (elem, i) => {
if (!elem) {
return;
}
refs.current[i] = elem;
if (!positions.current[i]) positions.current[i] = { x: 0, y: 0 };
if (unsubscribes.current[i]) {
unsubscribes.current[i]();
}
const handleMouseDown = (e) => {
e.target.style.userSelect = "none";
setPressedIndex(i);
};
elem.addEventListener("mousedown", handleMouseDown);
unsubscribes.current[i] = () => {
elem.removeEventListener("mousedown", handleMouseDown);
};
};
useEffect(() => {
if (!pressedIndex && pressedIndex !== 0) {
return;
}
const handleMouseMove = throttle((event) => {
if (
!refs.current ||
refs.current.length === 0 ||
!positions.current ||
positions.current.length === 0
) {
return;
}
const pos = positions.current[pressedIndex];
const elem = refs.current[pressedIndex];
positions.current[pressedIndex] = onDrag({
x: pos.x + event.movementX,
y: pos.y + event.movementY
});
elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
});
const handleMouseUp = (e) => {
e.target.style.userSelect = "auto";
setPressedIndex(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
handleMouseMove.cancel();
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [pressedIndex, onDrag]);
return [handleRefs, pressedIndex];
};
/// example.ts
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
};
const DraggableComponent = () => {
const handleDrag = ({ x, y }) => ({
x: Math.max(0, x),
y: Math.max(0, y)
});
const [handleRefs, pressedIndex] = useDraggable({
onDrag: handleDrag
});
return (
<>
{[1, 2, 3].map((el, i) => (
<div
key={"element" + i}
ref={(element) => handleRefs(element, i)}
style={quickAndDirtyStyle}
>
<p>{pressedIndex === i ? "Dragging..." : "Press to drag"}</p>
</div>
))}
</>
);
};
export default function App() {
return (
<div className="App">
<DraggableComponent />
</div>
);
}

In useDraggable function, create an Array of refs :
const refs = useRef([]);
const unsubscribe = useRef();
const legacyRef = useCallback((elem, index) => {
refs.current[index] = elem;
if (unsubscribe.current) {
unsubscribe.current();
}
if (!elem) {
return;
}
in handleMouseMove use this array :
if (!refs.current || !position.current) {
return;
}
const pos = position.current;
const elem = refs.current[index];
position.current = onDrag({
[...]
The idea is to use the array index to assign the ref to each element.
jHope it helps

Instead of trying to declare an array of refs try rendering a component while iterating over the array and declare single ref in that component.

Related

How to useEffect pause setTimeout on handleMouseEnter event. Continue setTimeOout on handleMouseLeaveEvent?

I am trying to figure out how i can use the handleMouseEnter/Leave event to pause/continue the setTimeout. The rest of the code appears to be working fine for me.
function Education({ slides }) {
const [current, setCurrent] = useState(0);
const length = slides.length;
const timeout = useRef(null);
const [isHovering, setIsHovering] = useState(false);
useEffect(() => {
const nextSlide = () => {
setCurrent((current) => (current === length - 1 ? 0 : current + 1));
};
timeout.current = setTimeout(nextSlide, 3000);
return function () {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [current, length]);
function handleMouseEnter(e) {
setIsHovering(true);
console.log("is hovering");
}
function handleMouseLeave(e) {
setIsHovering(false);
console.log("not hovering");
}
}
Hey there you can do this by this simple implementation.
const {useEffect, useState, useRef} = React;
const Education = () => {
const slides = [1,2,3,4,5,6];
const [current, setCurrent] = useState(0);
const length = slides.length;
const timeout = useRef(null);
const [isHovering, setIsHovering] = useState(false);
useEffect(() => {
const nextSlide = () => {
setCurrent((current) => (current === length - 1 ? 0 : current + 1));
};
if ( !isHovering)
timeout.current = setTimeout(nextSlide, 2000);
return function () {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [current, length, isHovering]);
function handleMouseEnter(e) {
// stop the timeout function to be set
setIsHovering(true);
// clear any existing timeout functions
if ( timeout.current ){
clearTimeout(timeout.current);
}
}
function handleMouseLeave(e) {
// to trigger the useeffect function
setIsHovering(false);
}
return(
<div>
{
slides.map( (s, i) => {
if ( i === current){
return <div key={i} style={{padding:"2em", backgroundColor:"gray", fontSize:"2em"}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>{s}</div>
}
})
}
</div>
)
}
ReactDOM.render(<Education />, document.querySelector("#app"))
You can check out in JsFiddle

ReactJS : eventListener tranistionend not getting cleaned up in useEffect

I have an image carousel component which has a smooth transition between images using the eventListener transtionend.
This event listener even though I have a cleanup function in place it creates a memory leak.
When I leave the page that has the image carousel the error does not appear yet. However, if I return to the page with the carousel and the transition completes one cycle (the image changes) then I get the error in the console.
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a
useEffect cleanup function.
I attached my code below:
/** #jsx jsx */
import { useState, useEffect, useRef } from "react";
import { css, jsx } from "#emotion/core";
import SliderContent from "./SliderContent";
import Slide from "./Slide";
import Arrow from "./Arrow";
import Dots from "./Dots";
export default function Slider({ autoPlay }) {
const getWidth = () => window.innerWidth * 0.8;
const slides = [
"https://images.unsplash.com/photo-1449034446853-66c86144b0ad?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2100&q=80",
"https://images.unsplash.com/photo-1470341223622-1019832be824?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2288&q=80",
"https://images.unsplash.com/photo-1448630360428-65456885c650?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2094&q=80",
"https://images.unsplash.com/photo-1534161308652-fdfcf10f62c4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2174&q=80",
];
const firstSlide = slides[0];
const secondSlide = slides[1];
const lastSlide = slides[slides.length - 1];
const [isTabFocused, setIsTabFocused] = useState(true);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const [state, setState] = useState({
translate: 0,
transition: 0.9,
activeSlide: 0,
_slides: [firstSlide, secondSlide, lastSlide],
});
const { activeSlide, translate, _slides, transition } = state;
const autoPlayRef = useRef();
const transitionRef = useRef();
const resizeRef = useRef();
const focusedTabRef = useRef();
const blurredTabRef = useRef();
useEffect(() => {
//eslint-disable-next-line react-hooks/exhaustive-deps
if (transition === 0) setState({ ...state, transition: 0.9 });
}, [transition]);
useEffect(() => {
transitionRef.current = smoothTransition;
resizeRef.current = handleResize;
focusedTabRef.current = handleFocus;
blurredTabRef.current = handleBlur;
autoPlayRef.current = handleAutoPlay;
});
useEffect(() => {
const play = () => autoPlayRef.current();
let interval = null;
if (autoPlay) {
interval = setInterval(play, autoPlay * 1000);
}
return () => {
if (autoPlay) {
clearInterval(interval);
}
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [isButtonDisabled, autoPlay]);
useEffect(() => {
const smooth = (e) => {
if (typeof e.target.className === "string" || e.target.className instanceof String) {
if (e.target.className.includes("SliderContent")) {
transitionRef.current();
}
}
};
const resize = () => resizeRef.current();
const onFocusAction = () => focusedTabRef.current();
const onBlurAction = () => blurredTabRef.current();
const transitionEnd = window.addEventListener("transitionend", smooth);
const onResize = window.addEventListener("resize", resize);
const onFocus = window.addEventListener("focus", onFocusAction);
const onBlur = window.addEventListener("blur", onBlurAction);
return () => {
window.removeEventListener("resize", onResize);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
window.removeEventListener("transitionend", transitionEnd);
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isButtonDisabled) {
const buttonTimeout = setTimeout(() => {
setIsButtonDisabled(false);
}, 1000);
return () => clearTimeout(buttonTimeout);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [isButtonDisabled]);
const handleFocus = () => setIsTabFocused(true);
const handleBlur = () => setIsTabFocused(false);
const handleAutoPlay = () => isTabFocused && nextSlide();
const handleResize = () => setState({ ...state, translate: getWidth(), transition: 0 });
const nextSlide = () => {
if (!isButtonDisabled) {
setState({
...state,
translate: translate + getWidth(),
activeSlide: activeSlide === slides.length - 1 ? 0 : activeSlide + 1,
});
}
setIsButtonDisabled(true);
};
const prevSlide = () => {
if (!isButtonDisabled) {
setState({
...state,
translate: 0,
activeSlide: activeSlide === 0 ? slides.length - 1 : activeSlide - 1,
});
}
setIsButtonDisabled(true);
};
const smoothTransition = () => {
let _slides = [];
// We're at the last slide.
if (activeSlide === slides.length - 1)
_slides = [slides[slides.length - 2], lastSlide, firstSlide];
// We're back at the first slide. Just reset to how it was on initial render
else if (activeSlide === 0) _slides = [lastSlide, firstSlide, secondSlide];
// Create an array of the previous last slide, and the next two slides that follow it.
else _slides = slides.slice(activeSlide - 1, activeSlide + 2);
setState({
...state,
_slides,
transition: 0,
translate: getWidth(),
});
};
return (
<div css={SliderCSS}>
<SliderContent
translate={translate}
transition={transition}
width={getWidth() * _slides.length}
>
{_slides.map((slide, i) => (
<Slide width={getWidth()} key={slide + i} content={slide} />
))}
</SliderContent>
<Arrow direction="left" handleClick={prevSlide} isDisabled={isButtonDisabled} />
<Arrow direction="right" handleClick={nextSlide} isDisabled={isButtonDisabled} />
<Dots slides={slides} activeIndex={activeSlide} />
</div>
);
}
const SliderCSS = css`
position: relative;
height: 600px;
width: 80%;
margin: 40px auto 0px auto;
overflow: hidden;
`;
The window listener is getting removed at the end of the useEffect but I don't know why it still creates the memory leak.
Hmm. It seems you're removing event listeners incorrectly. DOM addEventListener returns nothing (undefined).
Wrong:
const onResize = window.addEventListener("resize", resize);
window.removeEventListener("resize", onResize);
Should be:
window.addEventListener("resize", resize);
window.removeEventListener("resize", resize);

Problem with useEffect and navbar active on scroll "To fix, cancel all subscriptions"

I'm trying to make active anchors in navbar navigation on scroll. Everything is working until I don't change page and return back to home page, then when I scroll page I get an error from useEffect hook " Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. " How I should cancel all subscriptions ?
useEffect code :
const [headerText, setHeader] = useState(false);
let mount = false;
useEffect(() => {
if (!mount) {
scrollActiveNav();
scrollStickyNav((header) => {
setHeader(header);
});
}
return () => {
mount = true;
};
}, []);
Sticky navbar function :
const scrollStickyNav = (cb) => {
const scrollSticky = window.addEventListener("scroll", () => {
const header = document.getElementById("navbar");
if (window.pageYOffset >= 80) {
header.classList.add("navbar-sticky");
header.classList.remove("absolute");
cb(true);
} else {
header.classList.remove("navbar-sticky");
header.classList.add("absolute");
cb(false);
}
});
return window.removeEventListener("scroll", scrollSticky);
}
Acitve link anchor in navabar function:
const scrollActiveNav = () => {
const activeNav = window.addEventListener('DOMContentLoaded', () => {
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.add('active');
} else {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.remove('active');
}
});
}, options);
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
return window.removeEventListener("DOMContentLoaded", activeNav);
}
Try change this line let mount = false; for this const mount = useRef(false).
const [headerText, setHeader] = useState(false);
let mount = useRef(false);
useEffect(() => {
if (!mount.current) {
scrollActiveNav();
scrollStickyNav((header) => {
setHeader(header);
});
mount.current = true;
}
}, []);
Did you try to do something like this?
useEffect(() => {
scrollActiveNav();
const activeNav = window.addEventListener('DOMContentLoaded', () => {
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.add('active');
} else {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.remove('active');
}
});
}, options);
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
return () => {
window.removeEventListener("DOMContentLoaded", activeNav);
};
}, []);

How do i prevent unnecessary rerendering in React using useMemo or useCallback?

I´m trying to recreate a dijkstras pathfinding visualizer using react hooks.
The wrapper component is as below
import React, { useState, useEffect, useCallback, useRef } from "react";
import Node from "../Node/Node";
import "./PathfindingVisualizer.css";
import { dijkstra, getNodesInShortestPathOrder } from "../algorithms/dijkstras";
const START_NODE_ROW = 0;
const START_NODE_COL = 0;
const FINISH_NODE_ROW = 0;
const FINISH_NODE_COL = 3;
const TOTAL_ROWS = 3;
const TOTAL_COLS = 6;
const PathfindingVisualizer = () => {
const [nodeGrid, setNodeGrid] = useState({
grid: []
});
const mouseIsPressed = useRef(false);
useEffect(() => {
const grid1 = getInitialGrid();
setNodeGrid({ ...nodeGrid, grid: grid1 });
}, []);
const handleMouseDown = useCallback((row, col) => {
//console.log(newGrid);
setNodeGrid(prevGrid => ({
grid: getNewGridWithWallToggled(prevGrid.grid, row, col)
}));
mouseIsPressed.current = true;
//console.log(nodeGrid);
}, []);
// function handleMouseDown(row, col) {
// const newGrid = getNewGridWithWallToggled(nodeGrid.grid, row, col);
// console.log(newGrid);
// setNodeGrid({...nodeGrid, nodeGrid[row][col]= newGrid});
// }
const handleMouseEnter = useCallback((row, col) => {
//console.log(mouseIsPressed);
if (mouseIsPressed.current) {
setNodeGrid(prevNodeGrid => ({
...prevNodeGrid,
grid: getNewGridWithWallToggled(prevNodeGrid.grid, row, col)
}));
}
}, []);
const handleMouseUp = useCallback(() => {
mouseIsPressed.current = false;
}, []);
const animateDijkstra = (visitedNodesInOrder, nodesInShortestPathOrder) => {
for (let i = 0; i <= visitedNodesInOrder.length; i++) {
if (i === visitedNodesInOrder.length) {
setTimeout(() => {
animateShortestPath(nodesInShortestPathOrder);
}, 10 * i);
return;
}
setTimeout(() => {
const node = visitedNodesInOrder[i];
document.getElementById(`node-${node.row}-${node.col}`).className =
"node node-visited";
}, 10 * i);
}
};
const animateShortestPath = nodesInShortestPathOrder => {
for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
setTimeout(() => {
const node = nodesInShortestPathOrder[i];
document.getElementById(`node-${node.row}-${node.col}`).className =
"node node-shortest-path";
}, 50 * i);
}
};
const visualizeDijkstra = () => {
const grid = nodeGrid.grid;
console.log(grid);
const startNode = grid[START_NODE_ROW][START_NODE_COL];
const finishNode = grid[FINISH_NODE_ROW][FINISH_NODE_COL];
const visitedNodesInOrder = dijkstra(grid, startNode, finishNode);
const nodesInShortestPathOrder = getNodesInShortestPathOrder(finishNode);
animateDijkstra(visitedNodesInOrder, nodesInShortestPathOrder);
};
//console.log(nodeGrid.grid);
//console.log(visualizeDijkstra());
return (
<>
<button onClick={visualizeDijkstra}>
Visualize Dijkstra´s Algorithm
</button>
<div className="grid">
test
{nodeGrid.grid.map((row, rowIdx) => {
return (
<div className="row" key={rowIdx}>
{row.map((node, nodeIdx) => {
const { row, col, isStart, isFinish, isWall } = node;
return (
<Node
key={nodeIdx}
col={col}
row={row}
isStart={isStart}
isFinish={isFinish}
isWall={isWall}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseUp={handleMouseUp}
/>
);
})}
</div>
);
})}
</div>
</>
);
};
export default PathfindingVisualizer;
//----------------------------------------------------------
const getInitialGrid = () => {
const grid = [];
for (let row = 0; row < TOTAL_ROWS; row++) {
const currentRow = [];
for (let col = 0; col < TOTAL_COLS; col++) {
currentRow.push(createNode(col, row));
}
grid.push(currentRow);
}
return grid;
};
const createNode = (col, row) => {
return {
col,
row,
isStart: row === START_NODE_ROW && col === START_NODE_COL,
isFinish: row === FINISH_NODE_ROW && col === FINISH_NODE_COL,
distance: Infinity,
isVisited: false,
isWall: false,
previousNode: null
};
};
const getNewGridWithWallToggled = (grid, row, col) => {
const newGrid = grid.slice();
const node = newGrid[row][col];
const newNode = {
...node,
isWall: !node.isWall
};
newGrid[row][col] = newNode;
return newGrid;
};
My Codesandbox: https://codesandbox.io/s/twilight-bird-2f8hc?file=/src/PathfindingVisualizer/PathfindingVisualizer.jsx
VideoTutorial for reference:https://www.youtube.com/watch?v=msttfIHHkak
On first render a grid is generated from mapping over an two-dimensinal array, including a start node and a finish node.
If you click and drag onto the grid walls are toggled on/off, however this causes the entire grid to be rerendered twice, although only the node that is modified in the process should be rerendered.
I can´t figure out how to only rerender the node if the props that are passed down change.
The issue with your re-rendering were because even though you use useCallback method, you were actually re-creating the functions when nodeGrid changes and hence were not able to leverage the performance optimization from React.memo on Node component which is because all your onMouseDown, onMouseEnter, onMouseLeave handlers were recreated
Also when you use mouseIsPressed as a state, you were forced to trigger a re-render and recreate callbacks again because of it.
The solutions here is to make use of state update callbacks and also use mouseIsPressed as a ref and not a state
const [nodeGrid, setNodeGrid] = useState({
grid: []
});
const mouseIsPressed = useRef(false);
useEffect(() => {
const grid1 = getInitialGrid();
setNodeGrid({ ...nodeGrid, grid: grid1 });
}, []);
const handleMouseDown = useCallback((row, col) => {
//console.log(newGrid);
setNodeGrid(prevGrid => ({
grid: getNewGridWithWallToggled(prevGrid.grid, row, col)
}));
mouseIsPressed.current = true;
//console.log(nodeGrid);
}, []);
const handleMouseEnter = useCallback((row, col) => {
//console.log(mouseIsPressed);
if (mouseIsPressed.current) {
setNodeGrid(prevNodeGrid => ({
...prevNodeGrid,
grid: getNewGridWithWallToggled(prevNodeGrid.grid, row, col)
}));
}
}, []);
const handleMouseUp = useCallback(() => {
mouseIsPressed.current = false;
}, []);
Optimized DEMO

Access old state to compare with new state inside useEffect react hook with custom hooks usePrevious

I am trying to migrate my class based react component to react-hooks. The purpose of the component is to fetch stories from HackerNews API and after each 5000 milliseconds to do a polling by hitting the API again for new data.
The problem I am facing is in using the custom hooks below usePrevious() to compare my previous state with current state and only after the comparison to execute some other function inside useEffect()
I am most probably missing some basic implementation here of the custom hooks or of useEffect()
And I am following this official guide
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Here's the code for my class based component and this is perfectly working.
And below is my hooks based component
The problem is with this line
const fromPrevStoriesIds = usePrevious(prevStoriesIds);
The variable fromPrevStoriesIds is giving me good value inside return(), but inside useEffect() its undefined.
import React, { Component, useState, useEffect, useRef } from "react";
import axios from "axios";
import MUIDataTable from "mui-datatables";
import "./Dashboard.css";
import NewItemAddedConfirmSnackbar from "./NewItemAddedConfirmSnackbar";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
const isEqual = require("lodash.isequal");
const differenceWith = require("lodash.differencewith");
const omit = require("lodash.omit");
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const getEachStoryGivenId = (id, index) => {
return new Promise((resolve, reject) => {
axios
.get(`https://hacker-news.firebaseio.com/v0/item/${id}.json`)
.then(res => {
let story = res.data;
let result = omit(story, ["descendants", "time", "id", "type"]);
if (
result &&
Object.entries(result).length !== 0 &&
result.constructor === Object
) {
resolve(result);
} else {
reject(new Error("No data received"));
}
});
});
};
const Dashboard = () => {
const [prevStoriesIds, setPrevStoriesIds] = useState([]);
const [fetchedData, setFetchedData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [tableState, setTableState] = useState({});
const [
openNewItemAddedConfirmSnackbar,
setOpenNewItemAddedConfirmSnackbar
] = useState(false);
const [noOfNewStoryAfterPolling, setNoOfNewStoryAfterPolling] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const onChangeRowsPerPage = rowsPerPage => {
setRowsPerPage(rowsPerPage);
};
const closeNewItemConfirmSnackbar = () => {
setOpenNewItemAddedConfirmSnackbar(false);
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
setPrevStoriesIds(storyIds.data.slice(0, 2));
getAllNewStory(storyIds);
});
};
const getAllNewStory = storyIds => {
setIsLoading(true);
let topStories = storyIds.data.slice(0, 2).map(getEachStoryGivenId);
let results = Promise.all(topStories);
results
.then(res => {
setFetchedData(res);
setIsLoading(false);
})
.catch(err => {
console.log(err);
});
};
const fromPrevStoriesIds = usePrevious(prevStoriesIds);
useEffect(() => {
const fetchData = () => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
// console.log("STORY IDs FETCHED ", storyIds.data.slice(0, 2));
setPrevStoriesIds(storyIds.data.slice(0, 2));
getAllNewStory(storyIds);
});
};
fetchData();
const doPolling = () => {
var timer = setInterval(() => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
console.log(
"fromPrevStoriesIds INSIDE doPolling() ",
fromPrevStoriesIds
);
if (
fromPrevStoriesIds !== undefined &&
!isEqual(fromPrevStoriesIds.sort(), storyIds.data.slice(0, 2).sort())
) {
setPrevStoriesIds(storyIds.data.slice(0, 2));
setNoOfNewStoryAfterPolling(
differenceWith(
prevStoriesIds.sort(),
storyIds.data.slice(0, 2).sort(),
isEqual
).length
);
getAllNewStory(storyIds);
setOpenNewItemAddedConfirmSnackbar(true);
}
});
}, 5000);
};
doPolling();
// return () => {
// console.log("cleaning up");
// clearInterval(timer);
// };
}, [rowsPerPage, noOfNewStoryAfterPolling]);
let renderedStoriesOnPage = [];
const getDataToRender = (() => {
renderedStoriesOnPage = fetchedData.map(i => {
return Object.values(i);
});
return renderedStoriesOnPage;
})();
const columnsOptions = [
{
name: "Author",
sortDirection: tableState
? tableState.columns && tableState.columns[0].sortDirection
: null
},
{
name: "score",
sortDirection: tableState
? tableState.columns && tableState.columns[1].sortDirection
: null
},
{
name: "title",
sortDirection: tableState
? tableState.columns && tableState.columns[2].sortDirection
: null
},
{
name: "URL",
options: {
filter: false,
customBodyRender: (value, tableMeta, updateValue) => {
// console.log("TABLE META IS ", JSON.stringify(tableMeta));
return (
<a target="_blank" href={value}>
{value}
</a>
);
}
}
}
];
const options = {
filter: true,
selectableRows: false,
filterType: "dropdown",
responsive: "stacked",
selectableRows: "multiple",
rowsPerPage: tableState ? tableState.rowsPerPage : 10,
onChangeRowsPerPage: onChangeRowsPerPage,
activeColumn: tableState ? tableState.activeColumn : 0,
onTableChange: (action, tableState) => {
// console.log("taBLE STATE IS ", JSON.stringify(tableState));
setTableState(tableState);
}
};
return (
<React.Fragment>
{console.log("fromPrevStoriesIds INSIDE RETURN --- ", fromPrevStoriesIds)}
<div
style={{
marginLeft: "15px",
marginTop: "80px",
display: "flex",
flexDirection: "row"
}}
>
<h4 style={{ width: "400px", paddingRight: "15px" }}>
Hacker News top 2
</h4>
</div>
<div>
{isLoading ? (
<div className="interactions">
<div className="lds-ring">
<div />
<div />
<div />
<div />
</div>
</div>
) : fetchedData.length !== 0 && renderedStoriesOnPage.length !== 0 ? (
<MUIDataTable
title={"Hacker News API top 2 result"}
data={renderedStoriesOnPage}
columns={columnsOptions}
options={options}
/>
) : null}
<NewItemAddedConfirmSnackbar
openNewItemAddedConfirmSnackbar={openNewItemAddedConfirmSnackbar}
closeNewItemConfirmSnackbar={closeNewItemConfirmSnackbar}
noOfNewStoryAfterPolling={noOfNewStoryAfterPolling}
/>
</div>
</React.Fragment>
);
};
export default Dashboard;
Instead of returning ref.current from usePrevious return, ref since ref.current will be mutated at its reference and you will be able to receive the updated value within useEffect otherwise it will receive the value from its closure
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref;
}
Code:
const fromPrevStoriesIds = usePrevious(prevStoriesIds);
useEffect(() => {
const fetchData = () => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
// console.log("STORY IDs FETCHED ", storyIds.data.slice(0, 2));
setPrevStoriesIds(storyIds.data.slice(0, 2));
getAllNewStory(storyIds);
});
};
fetchData();
const doPolling = () => {
var timer = setInterval(() => {
axios
.get("https://hacker-news.firebaseio.com/v0/newstories.json")
.then(storyIds => {
console.log(
"fromPrevStoriesIds INSIDE doPolling() ",
fromPrevStoriesIds.current
);
if (
fromPrevStoriesIds.current !== undefined &&
!isEqual(fromPrevStoriesIds.current.sort(), storyIds.data.slice(0, 2).sort())
) {
setPrevStoriesIds(storyIds.data.slice(0, 2));
setNoOfNewStoryAfterPolling(
differenceWith(
prevStoriesIds.sort(),
storyIds.data.slice(0, 2).sort(),
isEqual
).length
);
getAllNewStory(storyIds);
setOpenNewItemAddedConfirmSnackbar(true);
}
});
}, 5000);
};
doPolling();
// return () => {
// console.log("cleaning up");
// clearInterval(timer);
// };
}, [rowsPerPage, noOfNewStoryAfterPolling]);

Categories

Resources