I am passing the increment function to a list of children (Row), but the count is never actually changed, I know that something about doing this in the children's useEffect is off. But I am still not able to understand this behavior.
Also, I am not setting the dependency array, because in this case, the count will run infinitely.
import { useCallback, useEffect, useState } from "react";
import "./styles.css";
const list = ["One", "Two", "Three"];
export default function App() {
const [count, setCount] = useState(0);
const handleOnClick = useCallback(() => {
setCount(count + 1);
}, [count]);
useEffect(() => {
if (list.length === count) {
alert("yaaay!");
}
}, [count]);
return (
<div className="App">
<h1>Count is: {count}</h1>
{list.map((item) => (
<Row key={item} name={item} addOne={handleOnClick} />
))}
</div>
);
}
const Row = ({ addOne, name }) => {
useEffect(() => {
addOne();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <p>{name}</p>;
};
The output is:
Count is: 1
One
Two
Three
Expected:
Count is: 3
One
Two
Three
okay i've created a demo
Couple of things. first, you need to set the useState values through the callback function since we are updating the counter value continuously
Secondly, need to use useRef in the child component to make sure child component is visited In each iteration. Do the checking inside child component useEffect
export default function App() {
const [count, setCount] = React.useState(0);
const handleOnClick = React.useCallback(() => {
setCount((count) => count + 1);
}, [count, setCount]);
React.useEffect(() => {
if (list.length === count) {
alert('yaaay!');
}
}, [count]);
return (
<div className="App">
<h1>Count is: {count}</h1>
{list.map((item) => (
<Row key={item} name={item} addOne={handleOnClick} />
))}
</div>
);
}
const Row =({ addOne, name }) => {
const dataFetchedRef = React.useRef(false);
React.useEffect(() => {
if (dataFetchedRef.current) return;
dataFetchedRef.current = true;
addOne();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name]);
return <p>{name}</p>;
};
Related
I've been stuck with this idea of setting the array to an empty one after the function fires, but no matter what I did the results were the same. I want to empty an array if it contains the element that was pressed, but It keeps logging the old array, making it impossible to check if array already contains it in the first place. I'm new to React and I've already tried using useEffect() but I don't understand it that much even after studying and trying to use it several times.
import React, { useState, useEffect } from "react";
import { Card } from "./Card";
import "../styles/Playboard.css";
import { Scoreboard } from "./Scoreboard";
export function Playboard(props) {
const [score, setScore] = useState(0);
const [bestScore, setBestScore] = useState(0);
const [clicked, setClicked] = useState([]);
const [cards, setCards] = useState([
<Card name="A" key={1} handleCardClick={handleCardClick}></Card>,
<Card name="B" key={2} handleCardClick={handleCardClick}></Card>,
<Card name="C" key={3} handleCardClick={handleCardClick}></Card>,
<Card name="D" key={4} handleCardClick={handleCardClick}></Card>,
<Card name="E" key={5} handleCardClick={handleCardClick}></Card>,
<Card name="F" key={6} handleCardClick={handleCardClick}></Card>,
]);
const increment = () => {
setScore((c) => c + 1);
};
function checkScore() {
if (score > bestScore) {
setBestScore(score);
}
}
function handleStateChange(state) {
setClicked(state);
}
useEffect(() => {
setCards(shuffleArray(cards));
handleStateChange();
}, []);
useEffect(() => {
setCards(shuffleArray(cards));
checkScore();
}, [score]);
function handleCardClick(e) {
console.log(clicked);
if (clicked.includes(e.target.querySelector("h1").textContent)) {
console.log(clicked);
resetGame();
} else {
increment();
clicked.push(e.target.querySelector("h1").textContent);
console.log(clicked);
}
}
function resetGame() {
setScore(0);
setClicked([]);
console.log(clicked);
}
const shuffleArray = (array) => {
return [...array].sort(() => Math.random() - 0.5);
};
return (
<div>
<div className="container">{cards}</div>
<Scoreboard score={score} bestScore={bestScore}></Scoreboard>
</div>
);
}
Looks like this is a memory game of sorts, where you have to remember which cards you've clicked and they shuffle after every click? Okay!
As my comment says, you don't want to put JSX elements in state; state should be just data, and you render your UI based on that.
Here's a cleaned-up version of your game (here on CodeSandbox) –
Card is a simple component that renders the name of the card, and makes sure clicking the button calls handleCardClick with the name, so you need not read it from the DOM.
shuffleArray doesn't need to be in the component, so it's... not.
initializeCards returns a shuffled copy of the A-F array.
useState(initializeCards) takes advantage of the fact that useState allows a function initializer; i.e. initializeCards will be called once by React when Playboard mounts.
useEffect is used to (possibly) update bestScore when score updates. You don't really need it for anything else in this.
cards.map(... => <Card...>) is the usual React idiom to take data and turn it into elements.
I'm using JSON.stringify() here as a poor man's scoreboard plus debugging tool (for clicked).
I'm making liberal use of the functional update form of setState for efficiency (not that it matters a lot in this).
import React, { useState, useEffect } from "react";
function Card({ name, handleCardClick }) {
return <button onClick={() => handleCardClick(name)}>{name}</button>;
}
function shuffleArray(array) {
return [...array].sort(() => Math.random() - 0.5);
}
function initializeCards() {
return shuffleArray(["A", "B", "C", "D", "E", "F"]);
}
function Playboard() {
const [score, setScore] = useState(0);
const [bestScore, setBestScore] = useState(0);
const [clicked, setClicked] = useState([]);
const [cards, setCards] = useState(initializeCards);
useEffect(() => {
setBestScore((bestScore) => Math.max(bestScore, score));
}, [score]);
function handleCardClick(name) {
if (clicked.includes(name)) {
resetGame();
} else {
setScore((c) => c + 1);
setClicked((c) => [...c, name]);
setCards((cards) => shuffleArray(cards));
}
}
function resetGame() {
setScore(0);
setClicked([]);
setCards(initializeCards());
}
return (
<div>
<div className="container">
{cards.map((name) => (
<Card name={name} key={name} handleCardClick={handleCardClick} />
))}
</div>
{JSON.stringify({ score, bestScore, clicked })}
</div>
);
}
I have got a dependency imageNo in useEffect() as I want the element to go up when it's being hidden, but scrollIntoView() does not work properly whenever imageNo changes, but it works when clicking a button.
Updated
import React, { useEffect, useRef, useState } from 'react';
const Product = ({ product }) => {
const moveRef = useRef(product.galleryImages.edges.map(() => React.createRef()));
const [imageNo, setImageNo] = useState(0);
useEffect(() => {
const position = moveRef.current[imageNo]?.current.getBoundingClientRect().y;
console.log('imageNo', imageNo); // <<<<----- This is also called whenever scrolling excutes.
if (position > 560) {
moveRef.current[imageNo]?.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [imageNo]);
const test = () => {
const position = moveRef.current[imageNo]?.current.getBoundingClientRect().y;
if (position > 560) {
moveRef.current[imageNo]?.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
// This changes `imageNo`
const handleScroll = () => {
let id = 0;
console.log('refs.current[id]?.current?.getBoundingClientRect().y', refs.current[id]?.current?.getBoundingClientRect().y);
const temp = imgArr?.find((el, id) => refs.current[id]?.current?.getBoundingClientRect().y >= 78);
if (!temp) id = 0;
else id = temp.id;
if (refs.current[id]?.current?.getBoundingClientRect().y >= 78) {
setImageNo(id);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div className="flex flex-row layout-width ">
{/* aside */}
<div className="sticky flex self-start top-[76px] overflow-y-auto !min-w-[110px] max-h-[90vh]">
<div className="">
{product.galleryImages.edges.map((image, i) => {
return (
<div ref={moveRef.current[i]} key={image.node.id}>
<Image />
</div>
);
})}
</div>
</div>
<button onClick={test}>btn</button>
</div>
);
};
export default Product;
Any suggestion will be greatly appreciated.
I couldn't see where the imageNo is coming from?
If it is just a normal javascript variable then it wouldn't trigger re-render even after putting it inside the useEffect's dependencies array.
If you want to make the re-render happen based on imageNo then make a useState variable for imageNo and change it using setter function that useState provides.
Helpful note - read about useState and useEffect hooks in React.
Why is it that the correct count value can be obtained in setinterval after the first click, and then the transformation does not occur again?
import React, { useEffect, useState } from 'react';
const Demo1 = () => {
let [count, setCount] = useState(1);
const onCountClick = () => {
count += 1;
setCount(count);
};
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
}, []);
console.log(count);
return <button onClick={() => onCountClick()}>test</button>;
};
You are directly modifying the state. Instead do this:
setCount(count++)
React doen't really handle setInterval that smoothly, you have to remember that when you put it in componentDidMount (useEffect with an empty dependencies' array), it builds its callback with the current values, then never updates.
Instead, put it inside componentDidUpdate (useEffect with relevant dependencies), so that it could have a chance to update. It boils down to actually clearing the old interval and building a new one.
const Demo1 = () => {
let [count, setCount] = useState(1);
let [intervalId, setIntervalId] = useState(null);
const onCountClick = () => {
count += 1;
setCount(count);
};
useEffect(() => {
setIntervalId(setInterval(() => {
console.log(count);
}, 1000));
}, []);
useEffect(() => {
clearInterval(intervalId);
setIntervalId(setInterval(() => {
console.log(count);
}, 1000));
}, [count]);
console.log(count);
return <button onClick={() => onCountClick()}>test</button>;
};
The first thing is that changing the value of state directly like count += 1 is a bad approach, instead use setCount(count + 1) and you cannot console.log any value in the return statement instead use {count} to display the value on the screen instead of console.
The following code will increment the value of count on every click instance
const [count, setCount] = useState(1);
const onCountClick = () => {
// count += 1;
setCount(count + 1);
};
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
});
return (
<div className="App">
<button onClick={() => onCountClick()}>test</button>;
</div>
);
Currently useEffect is fired when just one of the dependencies have changed.
How could I update it / use it to fire back when both ( or all ) of the dependencies have changed?
You'll need to add some logic to call your effect when all dependencies have changed. Here's useEffectAllDepsChange that should achieve your desired behavior.
The strategy here is to compare the previous deps with the current. If they aren't all different, we keep the previous deps in a ref an don't update it until they are. This allows you to change the deps multiple times before the the effect is called.
import React, { useEffect, useState, useRef } from "react";
// taken from https://usehooks.com/usePrevious/
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function useEffectAllDepsChange(fn, deps) {
const prevDeps = usePrevious(deps);
const changeTarget = useRef();
useEffect(() => {
// nothing to compare to yet
if (changeTarget.current === undefined) {
changeTarget.current = prevDeps;
}
// we're mounting, so call the callback
if (changeTarget.current === undefined) {
return fn();
}
// make sure every dependency has changed
if (changeTarget.current.every((dep, i) => dep !== deps[i])) {
changeTarget.current = deps;
return fn();
}
}, [fn, prevDeps, deps]);
}
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffectAllDepsChange(() => {
console.log("running effect", [a, b]);
}, [a, b]);
return (
<div>
<button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
<button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
</div>
);
}
An alternate approach inspired by Richard is cleaner, but with the downside of more renders across updates.
function useEffectAllDepsChange(fn, deps) {
const [changeTarget, setChangeTarget] = useState(deps);
useEffect(() => {
setChangeTarget(prev => {
if (prev.every((dep, i) => dep !== deps[i])) {
return deps;
}
return prev;
});
}, [deps]);
useEffect(fn, changeTarget);
}
You'll have to track the previous values of your dependencies and check if only one of them changed, or both/all. Basic implementation could look like this:
import React from "react";
const usePrev = value => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const App = () => {
const [foo, setFoo] = React.useState(0);
const [bar, setBar] = React.useState(0);
const prevFoo = usePrev(foo);
const prevBar = usePrev(bar);
React.useEffect(() => {
if (prevFoo !== foo && prevBar !== bar) {
console.log("both foo and bar changed!");
}
}, [prevFoo, prevBar, foo, bar]);
return (
<div className="App">
<h2>foo: {foo}</h2>
<h2>bar: {bar}</h2>
<button onClick={() => setFoo(v => v + 1)}>Increment foo</button>
<button onClick={() => setBar(v => v + 1)}>Increment bar</button>
<button
onClick={() => {
setFoo(v => v + 1);
setBar(v => v + 1);
}}
>
Increment both
</button>
</div>
);
};
export default App;
Here is also a CodeSandbox link to play around.
You can check how the usePrev hook works elsewhere, e.g here.
I like #AustinBrunkhorst's soultion, but you can do it with less code.
Use a state object that is only updated when your criteria is met, and set it within a 2nd useEffect.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [ab, setAB] = useState({a, b});
useEffect(() => {
setAB(prev => {
console.log('prev AB', prev)
return (a !== prev.a && b !== prev.b)
? {a,b}
: prev; // do nothing
})
}, [a, b])
useEffect(() => {
console.log('both have changed')
}, [ab])
return (
<div className="App">
<div>Click on a button to increment its value.</div>
<button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
<button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
</div>
);
}
FWIW, react-use is a nice library of additional hooks for react that has ~30k stars on GitHub:
https://github.com/streamich/react-use
And one of those custom hooks is the useCustomCompareEffect:
https://github.com/streamich/react-use/blob/master/docs/useCustomCompareEffect.md
Which could be easily used to handle this kind of custom comparison
To demonstrate how you can compose hooks in various manners, here's my approach. This one doesn't invoke the effect in the initial attribution.
import React, { useEffect, useRef, useState } from "react";
import "./styles.css";
function usePrevious(state) {
const ref = useRef();
useEffect(() => {
ref.current = state;
});
return ref.current;
}
function useAllChanged(callback, array) {
const previousArray = usePrevious(array);
console.log("useAllChanged", array, previousArray);
if (previousArray === undefined) return;
const allChanged = array.every((state, index) => {
const previous = previousArray[index];
return previous !== state;
});
if (allChanged) {
callback(array, previousArray);
}
}
const randomIncrement = () => Math.floor(Math.random() * 4);
export default function App() {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const [state3, setState3] = useState(0);
useAllChanged(
(state, prev) => {
alert("Everything changed!");
console.info(state, prev);
},
[state1, state2, state3]
);
const onClick = () => {
console.info("onClick");
setState1(state => state + randomIncrement());
setState2(state => state + randomIncrement());
setState3(state => state + randomIncrement());
};
return (
<div className="App">
<p>State 1: {state1}</p>
<p>State 2: {state2}</p>
<p>State 3: {state3}</p>
<button onClick={onClick}>Randomly increment</button>
</div>
);
}
I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);