← Back
hooks react react-tapas

React Tapas 9: Hooks dependencies

Okay, let's face the most difficult part of hooks: dependencies.

Dependencies overview #

Dependencies determines "what has to change" to run a hook. They are usually expressed as an array, passed as last argument (in React's built in hooks)

1. Empty dependencies #

If we add an empty array of dependencies, our hook will be called just when the component is mounted:

useEffect(() => {
console.log('This will be called on mounting');
}, []) // <= empty array: no dependencies

2. No dependencies #

No dependencies is not the same as empty dependencies. No dependencies (aka: no array) means "call the hook always":

useEffect(() => {
console.log('This function will be called every render')
}) // <= look ma', no array

Some could think that an no dependencies effect is the same as calling the function directly inside rendering. And, in terms of number of execution, is the same (not exactly the same, react could do some optimizations and skip some effects).

But the main difference is that the function will be called after (outside) rendering, so it's best suited for things that could potentially delay/block rendering.

3. Some dependencies #

This is the most common scenario. The hook (the effect in this case) will be called initially when the component is mounted (same as "empty dependencies") and then, every time any of the dependency changes.

const [value, setValue] = useState();

useEffect(() => {
console.log('This will render every time the value changed', value);
}, [value])

Every time we call setValue (and actually setting a different value), the effect will be called.

Not so easy #

The above model is quite easy to understand, but it comes with bad news: "rules" of hooks enforced to declare all dependencies. Or said in another way: if we miss or skip a dependency, we are introducing potential bugs into our code.

One clarification: class based (or Angular or Vue) components adoleces (is that a word? 🤔) this same problem, this potential bug. But more hidden.

With hooks, the problem just can explode in your face (with the infinite render loop we saw at the video... and I'm not talking about the Meet infinite loop, that was quite cool).

Gimme one example #

A simple effect+state:

const App = () => {
const [id, setId] = useState(1)
const [post, setPost] = useState()

useEffect(() => {
fetch(`http://blog.com/post/${id}`).then(setPost)
}, []);

return (
<div>
<button onClick={() => setId(id + 1)}>Next</button>
<h1>{post.title}</h1>
<label>(id: {id})</label>
</div>
);
}

Initially we will se the title of post with id 1 and the "(id: 1)" label is shown.

When we press the button, the id is incremented and its value will be displayed correctly: "(id: 1)". But the hook/effect wont be called.

Notice one important thing here: we create an effect function every rendering. This new function is passed to useEffect, but since dependencies didn't change, it won't be executed.

Of course, we can solve it by adding the required dependency:

  useEffect(() => {
fetch(`http://blog.com/post/${id}`).then(setData)
}, [id]);

Now, when we press the button, the effect fn will be executed. Fine.

useQuery #

The example is (intentionally) very similar to the useQuery hook we've seen before. In fact, we can extract the whole component logic into a reusable function (one of the most important features of hooks, split components and business logic to reuse the later):

function useQuery(fetch) {
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(false);

useEffect(() => {
setLoading(true)
fetch()
.then(data)
.catch(error)
.finally(() => setLoading(false);
}, []);

return { data, error, loading }
}

The usage would be something like this:

const App = () => {
const [id, setId] = useState(1)
const { data: post } = useQuery(() => fetch(`http://blog.com/post/${id}`));
return (
<div>
<button onClick={() => setId(id + 1)}>Next</button>
<h1>{post.title}</h1>
<label>(id: {id})</label>
</div>
);
}

But unfortunately this is buggy code. Could you deduce what is going to happen when we press the button? For sure, the solution seems simple: adding the fetch function to dependencies:

  useEffect(() => {
setLoading(true)
fetch(...)
}, [fetch]);

But this is even worst. What is going to happen?

Next chapter #

We will see several ways to solve this kind of problems. Specially one (often unknown) solution.