Table of contents
Over the past several years, I've been on both sides of dozens of front-end job interviews. In my experience, when it comes to advanced ReactJS interview questions, there are 3 core concepts that can truly make a candidate stand out.
When does a React component re-render?
At first glance, it's a simple question. But the level of detail in the answer can reveal a lot about the candidate's understanding when it comes to advanced React concepts. At the most basic level, the answer is very straightforward - a component re-renders when there is a change in its state or props or if its parent re-renders. This is, in essence, true. But there is more to the story.
Props or state change
Let's start with the props / state change. What does it mean exactly? How is it evaluated? Will the below code trigger a re-render when you click the button?
import { useState } from 'react';
const Component = () => {
const [number, setNumber] = useState(1);
console.log('re-renders');
return <button onClick={() => setNumber(1)}>{number}</button>;
};
Nope. The value for number
is always the same. How about now?
import { useState } from 'react';
const Component = () => {
const [numberArray, setNumberArray] = useState([1]);
console.log('re-renders');
return <button onClick={() => setNumberArray([1])}>{numberArray[0]}</button>;
};
In this case the component re-renders on every button click. Interesting! Why is that?
Short answer - there's a difference between evaluating equality for primitive values (like numbers or strings) and objects (like arrays). Read more about referential equality in the context of React in my article about useEffect
. Senior React developers should be acutely aware of this nuance before they can move on to other advanced topics, like memoization. But more on that in a minute.
The parent component
Above we mention that a component re-renders when its parent re-renders. Is that true? Let's find out.
Will the component re-render when we click the button?
import { useState } from 'react';
const Child = () => {
console.log('re-renders');
return <p>I'm a child</p>;
};
const Parent = () => {
const [number, setNumber] = useState(1);
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
<Child />
</div>
);
};
export default Parent;
Yep! It behaves as expected. How about now?
const Child = () => {
console.log('re-renders');
return <p>I'm a child</p>;
};
const Parent = ({ children }) => {
const [number, setNumber] = useState(1);
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
{children}
</div>
);
};
const App = () => {
return (
<Parent>
<Child />
</Parent>
);
};
export default App;
Hm, what is going on here? The child doesn't re-render anymore even though the state of the parent is updated when the button is clicked. Why? Well, the children
technically didn't change in this case - so React can skip re-rendering this part of the component tree. It's, in essence, an optimisation technique. For a more in-dept explanation, check out Kent Dodds' excellent article.
React context and hooks
Another mechanism to cause a component to re-render is through React context. A change is the context value will trigger a re-render for all components that are consuming it. Let's take a look.
import { useState, useContext, createContext } from 'react';
const Context = createContext();
const Child1 = () => {
const { number, setNumber } = useContext(Context);
console.log('Child1 re-renders');
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
<p>I'm a child</p>
</div>
);
};
const Child2 = () => {
console.log('Child2 re-renders');
return <p>I'm a second child</p>;
};
const NumberContext = ({ children }) => {
const [number, setNumber] = useState(0);
const contextValue = { number, setNumber };
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
};
const App = () => {
return (
<NumberContext>
<Child1 />
<Child2 />
</NumberContext>
);
};
export default App;
In the example above Child1
is consuming the context which means clicking the button is causing a re-render. This is not the case for Child2
, so it doesn't re-render.
Much the same principle applies to React hooks.
import { useState } from 'react';
const useCount = () => {
const [number, setNumber] = useState(0);
return { number, setNumber };
};
const Child1 = () => {
const { number, setNumber } = useCount();
console.log('Child1 re-renders');
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
<p>I'm a child</p>
</div>
);
};
const Child2 = () => {
console.log('Child2 re-renders');
return <p>I'm a second child</p>;
};
const App = () => {
return (
<div>
<Child1 />
<Child2 />
</div>
);
};
export default App;
Once again, Child1
re-renders while not Child2
does not.
Component re-renders vs. DOM updates
While this is all very interesting and useful, one important question still remains - why should we care about component re-renders in the first place?
By now, we certainly have noticed that sometimes React re-renders our components even when no DOM updates are taking place. As experienced React developers, we probably already know (at least in broad strokes) about React reconciliation and the diffing algorithm that allows the library to do the minimum number of DOM updates needed to update the UI.
Still, these "empty" re-renders feel unnecessary. Should we get rid of them?
When to use useMemo
and useCallback
?
The question is relatively concrete but it links to the broader concept of memoization. This is a topic a lot of experienced React developers struggle with. Both hooks - useCallback
and useMemo
- serve as a mechanism to memoize functions and values respectively in a React component. This allows us to cache expensive calculations (see example below) and keep references stable across re-renders, provided none of the items in the dependency array changed.
What makes this question advanced is that it is predicated on the assumption that the candidate is already familiar with these hooks. The focus is rather on whether they can think critically about performance optimisations and understand not only the benefits but also the trade-offs. As React developers, once we learn we can optimise re-renders, there's often an irresistible urge to do it all the time. But every optimisation comes at a cost.
In this case there are, broadly speaking, at least two ways the cost might out-way the benefit:
- Using memoization when it's not really necessary might actually hurt performance. Here are some interesting measurements to illustrate this.
- Overusing
useMemo
anduseCallback
makes the code base notably more complex and less maintainable for the entire team.
So, to get back to the original question, when should we use useMemo
and useCallback
?
Short answer - when we have strong evidence that there is a real performance issue in our application and that applying these techniques will help solve this problem in a tangible, measurable way. Alternatively, the two hooks can be used when we need a stable reference for a value or a method, respectively. Unless one of these conditions is met, we do not have a good justification to use useMemo
and / or useCallback
.
To illustrate an obvious performance issue, let's look at a classic useMemo
example:
import { useState, useMemo } from 'react';
const Component = () => {
const [number, setNumber] = useState(0);
const expensiveCalculationResult = artificiallyExpensiveCalculation(1);
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
<p>Expensive calculation result: {expensiveCalculationResult}</p>
</div>
);
};
const ComponentWithUseMemo = () => {
const [number, setNumber] = useState(0);
const expensiveCalculationResult = useMemo(() => {
return artificiallyExpensiveCalculation(1);
}, []);
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
<p>Expensive calculation result: {expensiveCalculationResult}</p>
</div>
);
};
const App = () => {
return (
<div>
<Component />
<ComponentWithUseMemo />
</div>
);
};
export default App;
const artificiallyExpensiveCalculation = (number) => {
for (let i = 0; i < 1000000000; i++) {}
return number;
};
Because of the artificially expensive calculation we have created, it's easy to prove that useMemo
improves performance in this case.
A lot of the time, performance issues might manifest in more subtle ways
and require careful measurement and analysis before they can be resolved in the
right way. Whether the answer is useMemo
, useCallback
or another
optimisation technique altogether, taking advantage of the React dev tools
profiler is always a good first step to diagnose the problem.
When should we use custom React hooks?
Once again, this question appears deceptively simple on the surface. But there is a lot of advanced React-specific knowledge a candidate must have at their fingertips to answer it.
To come up with a specific use case and be able to justify it, they should be aware of the benefits of React hooks in general and custom hooks in particular. So what are they?
Here's a non-exhaustive list in no particular order:
- React hooks are notably easier to reason about compared to previous life-cycle methods. They let us collocate related logic and are, on average, less verbose
- React hooks allow us to re-use stateful logic across React component
- It's relatively straightforward to unit test React hooks independently from a React component
- React hooks are easy to use with Typescript
You can find out more about the motivation behind hooks in the official React docs.
Then, with all of the above in mind, the candidate must come up with a specific scenario in which these benefits truly shine. So what are some classic examples?
Event listeners
A lot of the time, we want to re-use some logic related to event listeners in our components. One example could be to create a useKeyPress
hook in order to detect that a certain key is pressed.
const useKeyPress = (targetKey) => {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);
return keyPressed;
};
For a good use case for this hook, check out my article on how to build keyboard accessible list in React.
Client-side data-fetching
Another great use case for custom hooks is data-fetching on the client. Libraries like React Query provide hooks like useQuery
and useMutation
to access and manage server state. In addition, it's often useful to build our own custom hooks for specific resources in order to simplify data-fetching even further. For example, to fetch a list of posts, we might create a usePosts
hook.
import { useQuery } from '@tanstack/react-query';
function usePosts() {
return useQuery(['posts'], async () => {
const { data } = await axios.get('https://myapi/posts');
return data;
});
}
Simplified usage of React Context
Let's have a closer look at our NumberContext
from the example above. Could we improve it? What if we created a useNumberContext
custom hook that would allow us to use it with ease.
import { useState, useContext, createContext } from 'react';
const Context = createContext();
const NumberContext = ({ children }) => {
const [number, setNumber] = useState(0);
const contextValue = { number, setNumber };
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
};
const useNumberContext = () => {
const { number, setNumber } = useContext(Context);
return { number, setNumber };
};
const Component = () => {
const { number, setNumber } = useNumberContext();
return (
<div>
<button onClick={() => setNumber(number + 1)}>{number}</button>
<p>I'm a child</p>
</div>
);
};
There are many, many other valid use cases for React hooks. What is offered here is just an illustration. The goal of this questions is not to receive a specific "correct" answer. Rather, it's meant to give candidates an opportunity to demonstrate that they can truly think in React .
Conclusion
As a candidate, having a good grasp of these core ReactJS concepts can come a long way in helping you stand out and ace an interview. Still, it's important to point out that this list is neither exhaustive nor comprehensive. The technical interview is also probably going to be only one step in a long interview process that usually involves building a React app. To tackle this next step, check out my article on how to submit a good React take-home assignment.
Happy coding! ✨