The second argument to React's useEffect
hook is an array of dependencies for your useEffect
callback. When any value in that array changes, the effect callback is re-run. But the variables
object we're passing to that array is created during render, so our effect will be re-run every render even if the shape of the object is the same. So let's solve this by doing our own equality check from within the effect callback.
Curious why you needed a second useEffect, Couldn't you of just run this at the bottom of your first useEffect
const previousInputs = useRef()
previousInputs.current = [query, variables]
That's a great point Dean! Could definitely use the same one. The reason I used a different one is probably because I knew that I was going to extract it to its own usePrevious
hook in the future 😅
Hi, this course seems to be a continuation of some other course. What course is this based on?
Hi Timothy 👋 It is not a continuation of any other course. But if you want a primer on hooks you can watch my playlist here: https://kcd.im/hooks-and-suspense
Nitpick, but feels weird to declare the previousInputs ref at the bottom. Also, I wonder if it'd be better to be able to pass a function as the second argument to useEffect that would act as the condition?
I wonder if it'd be better to be able to pass a function as the second argument to useEffect that would act as the condition?
Unless something's changed and I'm unaware, that's not how useEffect
works. It does not accept a function as the second argument.
My understanding of the const
keyword is that it will not be hoisted, thus referring previousInput ref before it is declared should throw an error.
Is it my understanding wrong?
Yes, you would be correct except I'm accessing the value of previousInput
in a closure that is called after render which is why it works. I did it this way because the effect needs to happen second and I wanted to keep things together.
Can we not do this with useState instead of useRef ? We can keep track of the previous variables in state. Would it be any different ?
How come previousInputs
is not undefined in the first useEffect
? You define it later in the code.
Okay, I saw the other answer but apparently I can't delete my previous comment.
function deepCompareEquals(a, b){
// TODO: implement deep comparison here
// something like lodash
// return _.isEqual(a, b);
}
function useDeepCompareMemoize(value) {
const ref = useRef()
// it can be done by using useMemo as well
// but useRef is rather cleaner and easier
if (!deepCompareEquals(value, ref.current)) {
ref.current = value
}
return ref.current
}
function useDeepCompareEffect(callback, dependencies) {
useEffect(callback, useDeepCompareMemoize(dependencies))
}
Hi Kent, can you demonstrate how to use useMemo
(by passing the flattened variables into the dependency array) in the component containing <Query />
and put the rendering of <Query />
inside the callback of useMemo
so that <Query />
doesn't get re-rendered if the variables doesn't change?
Hi Kent, Thanks for the course. Something doesn't click for me. In your useEffect
, you're sometimes returning without proceeding with setState
. To me, it seems like a violation of the Rules of Hooks https://reactjs.org/docs/hooks-rules.html) because the useState hook is somehow in a condition. Yet your code works and React doesn't complain. Is there a corner case where this wouldn't work?
@François, the hooks are still called in the same order, just last calls were omitted. So this should just work, unless React expect all hook calls to be there in the next render... not sure about that. It might worth trying to see if this code construct survives the eslint rule (eslint-plugin-react-hooks). If not we are sure not to use this code construct. However something seems wrong with the Github link, the code of this chapter doesn't showup.
Is it guaranteed that hooks run and finish in the order in which they are defined? That is, what happens if the useEffect
setting the previousInput
runs(& finishes) before the query useEffect
? In that case, previousInput
will be updated to the current prop values and the isEqual
check will be always true.
I guess this is not a problem in the current scenario because both useEffect
callbacks are synchronous until the use of previousInput
variable. Am I getting this correct?
@Avi I was curious about this too - it looks like useEffect
calls are batched just like setting state from a useState
hook (note if you're setting state inside an asynchronous method this is not the case)>
In the below code the component will only be re-rendered once after the initial render.
import React, { Fragment, useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function Component() {
const [a, setA] = useState("a");
const [b, setB] = useState("b");
const myRef = useRef();
console.log("a", a);
console.log("b", b);
console.log(JSON.stringify(myRef.current));
useEffect(() => {
if (a !== "aa") setA("aa");
myRef.current = { ...myRef.current, a: "a" };
});
useEffect(() => {
if (b !== "bb") setB("bb");
myRef.current = { ...myRef.current, b: "b" };
});
return (
<Fragment>
<div>a = {a}</div>
<div>b = {b}</div>
<div>myRef = {JSON.stringify(myRef)}</div>
</Fragment>
);
}
function App() {
return <Component />;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
your console output here will be:
a a
b b
undefined
a aa
b bb
{{ current: { a: "a", b: "b" }}