Reputation: 70466
I am playing around with React Hooks, calling a method (that mutates state) from two different useEffect. Following code is given:
function App() {
const [clicked, setClicked] = useState(false);
/**
* Listen for clicked changes. When clicked changes to true,
* allow setCounterAndSetUrlHash to do it's thing, before accpeting
* the next click. So the clicked flag is simply a valve, that opens
* after two seconds.
*/
useEffect(() => {
if (clicked) {
setCounterAndSetUrlHash(counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
const [counter, setCounter] = useState(0);
/**
* Listen for changes in the URL hash. When the user presses
* the back button in the browser toolbar, decrement the
* counter value.
*/
useEffect(() => {
window.onhashchange = () => {
const value = Number(window.location.hash.replace("#", ""));
// must be number
if (typeof value === "number" && value % 1 === 0) {
if (counter - 1 === value) {
setCounterAndSetUrlHash(counter - 1);
}
}
};
});
/**
* Set a new counter value and apply the same value
* to the URL hash. I want to reuse this function
* in both useEffect above.
*/
const setCounterAndSetUrlHash = value => {
setCounter(value);
if (value === 0) {
window.location.hash = "";
} else {
window.location.hash = String(value);
}
};
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
The code in action: https://codesandbox.io/s/dreamy-shadow-7xesm
The code is actually working. However I am getting this warning..
React Hook useEffect has a missing dependency: 'counter'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
.. and I am not sure how to conform with that while keeping the current functionality. When I add counter to the dependencies, I end up with an infinite loop.
Upvotes: 4
Views: 817
Reputation: 323
Add counter to the first useEffect():
const [counter, setCounter] = useState(0);
useEffect(() => {
if (clicked) {
setCounterAndSetUrlHash(counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked, counter]);
https://codesandbox.io/s/cold-sun-56u7j
Upvotes: 0
Reputation: 53994
Try using the functional setState, setState((state, props) => stateChange)
useEffect(() => {
if (clicked) {
setCounterAndSetUrlHash(counter => counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
Upvotes: 1
Reputation: 9682
To solve the issue of the onhashchange
callback using the first counter value I suggest to move the functionality to the callback of setCounter
. This would also imply that you need a different function for the button and the hash change.
Also set the variables and useState
definitions at the top, and after useEffect
which can make use of them. If you want a useEffect
to run only once, set an empty array of dependencies; leaving out dependecies will run on every render.
export const App = () => {
const [clicked, setClicked] = useState(false);
const [counter, setCounter] = useState(0);
/**
* Listen for clicked changes. When clicked changes to true,
* allow setCounterAndSetUrlHash to do it's thing, before accpeting
* the next click. So the clicked flag is simply a valve, that opens
* after two seconds.
*/
useEffect(() => {
if (clicked) {
setCounter(counter => {
const value = counter + 1;
if (value === 0) {
window.location.hash = "";
} else {
window.location.hash = String(value);
}
return value;
});
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
/**
* Listen for changes in the URL hash. When the user presses
* the back button in the browser toolbar, decrement the
* counter value.
*/
useEffect(() => {
window.onhashchange = e => {
const value = Number(window.location.hash.replace("#", ""));
// must be number
if (typeof value === "number" && value % 1 === 0) {
setCounter(counter => {
if (counter - 1 !== value) {
return counter;
}
if (value === 0) {
window.location.hash = "";
} else {
window.location.hash = String(value);
}
return value;
});
}
};
}, []);
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
};
Upvotes: 0
Reputation: 1909
Your first effect uses counter
state variable but its dependency list does not include it. Including it in dependency list will create infinite loop.
You can remove the dependency on counter
by using function type argument in setCounter
.
function App() {
const [clicked, setClicked] = useState(false);
/**
* Listen for clicked changes. When clicked changes to true,
* allow setCounterAndSetUrlHash to do it's thing, before accpeting
* the next click. So the clicked flag is simply a valve, that opens
* after two seconds.
*/
useEffect(() => {
if (clicked) {
incrCounter(1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
const [counter, setCounter] = useState(0);
/**
* Listen for changes in the URL hash. When the user presses
* the back button in the browser toolbar, decrement the
* counter value.
*/
useEffect(() => {
window.onhashchange = () => {
const value = Number(window.location.hash.replace("#", ""));
// must be number
if (typeof value === "number" && value % 1 === 0) {
if (counter - 1 === value) {
incrCounter(- 1);
}
}
};
});
useEffect(() => {
if (counter === 0) {
window.location.hash = "";
} else {
window.location.hash = String(counter);
}
}, [counter])
/**
* Set a new counter value and apply the same value
* to the URL hash. I want to reuse this function
* in both useEffect above.
*/
const incrCounter = delta => {
setCounter(value => value + delta);
};
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
Upvotes: 1