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.
Instructor: [00:00] With our old query component, we were actually using this is equal from Lodash when we had this componentDidUpdate to compare the previous query and the previous variables with the new query and the new variables. We are doing that because the variables can actually be an object.
[00:16] As we can see here, the variables is an object that is going to be created new every single time. If we look at our useEffect, we are comparing the query and the variables. React is doing that for us, but it's not going to do a shallow comparison.
[00:32] It's actually going to do a direct comparison. It's going to say, previous variables, which it is tracking itself, and it's going to check that against variables. If that is not true, then it's going to rerun our callback.
[00:45] That's not what we want. This is actually always going to return false, because the way that people are using our component is, they're passing this variables object so every single time it is a brand new object. Every single time, our useEffect callback is going to be called.
[01:01] We need to do a little bit of extra work to make sure that that doesn't happen. What I'm going to do is, I'm going to bring back import is equal from Lodash is equal. Then inside of my useEffect hook, I'm basically going to simulate the same kind of thing that I was doing in here, which is not typically what we need to do.
[01:22] In our situation, because we're comparing two objects, and we need to make sure that they are deep equal, and we need to do a deep equality check on these, we're going to have to do it this way.
[01:33] I'm going to say, if is equal. I need to get my previous inputs, and then my current inputs, which is query and variables. Then I'll actually get rid of this right here, so I'll have our effects callback called every time and we'll do our own check right here.
[01:51] If they are equal, then we'll return. Now we need to find some way to know what the previous inputs were. Every single time this query is run, those inputs are going to change and we need to keep a reference to what those previous inputs were. We need to keep a reference.
[02:05] Let's try use ref. Down here below our useEffect, we're going to say const previous inputs equals use ref, and we'll use effect. After every single time our component is rendered, we'll say, previous inputs.current equals query and variables. Awesome.
[02:26] Then we're going to use the previous input.current rather than just simply previous inputs, and we'll compare the previous inputs with the new query and variables. Doing that, we can be sure that our effect is called every single time, but that we don't actually run our set state calls and our client calls unless the previous inputs are different from the new inputs.
[02:48] In review, what we did to make this work was, we ensured that our effect is called every time by removing the inputs argument here. We created a ref to keep track of the previous inputs on every render. Then we compared the previous inputs with the new ones, and if they are changed, then we'll go ahead and run our client to rerun the query.
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" }}
Curious why you needed a second useEffect, Couldn't you of just run this at the bottom of your first useEffect