This guide is focusing on the recent versions of react featuring React Hooks.
TL;DR; They aren't just a decoration and do affect the way your React application works.
- See this Twitter thread for a quick overview: https://twitter.com/dan_abramov/status/1415279090446204929
- Article from the React's docs: https://reactjs.org/docs/reconciliation.html#recursing-on-children
Bad:
const Component = ({ items }) => (
<ul>
{items.map((item) => (
<li>{item.name}</li>
))}
</ul>
);
Good:
const Component = ({ items }) => (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
Maybe they hold an other value which can identify them? Lists tend to hold unique items: different text, colours. E.g., when dealing with an array of strings, consider filtering out duplicates. This will allow to safely use those strings as keys.
Example:
const items = ['a', 'a', 'b', 'c'];
const uniqueItems = Array.from(new Set(items));
// ^ Once filtered, the unique items are ready to use as keys.
It's tempting to use index
to quickly deal with key
property. Usually, there are better options but if we know the list is constant and not going to be altered (sorting, filtering, etc.), then index
might work just fine.
Example:
const Component = ({ items }) => (
<ul>
{new Array(3).fill(0).map((_, index) => (
<li key={index}>{index 1}. Item</li>
))}
</ul>
);
// ^ This component is supposed to always print 3 list items.
TL;DR; Doing so may cause unintended amount of re-renders and/or calling useEffect
functions.
const a = '2'
const b = '2'
a === a; // returns true
b === b; // returns true
// Comparing different variables
a === b; // returns true
However, objects (all things which aren't primitives) can't rely on such comparison:
const a = [];
const b = [];
a === a; // returns true
b === b; // returns true
// Comparing different variables
a === b; // returns false
It's important to keep that in mind when dealing with various values in React and providing them as "deps" for various hooks.
Here are the common patterns you might want to use in your components or custom hooks:
Bad:
const useMyHook = ({ values = [] }) => {
// ^ We provide an empty array to guard from `undefined`.
useEffect(function doSomethingWithValues() {
// Code fires when `value` changes.
}, [values])
};
Good:
const DEFAULT_VALUE = [];
const useMyHook = ({ values = DEFAULT_VALUE }) => {
// ^ This time `DEFAULT_VALUE` is alawyas _the same_.
useEffect(function doSomethingWithValues() {
// Code fires when `value` changes.
}, [value])
};
☝️ In the "bad" example the empty array would be assigned on each re-render resulting in firing the doSomethingWithValue
effect too often. The "good" example fixes that by assigning an empty array to a variable outside of the function.
It applies to this similar pattern as well:
Bad:
const MyComponent = () => {
const values = useValue(); // returns an array or null
const safeValues = values || [];
useEffect(function doSomethingWithValues() {
// Code fires when `value` changes.
}, [value])
return null;
}
Good:
const MyComponent = () => {
const values = useValue(); // returns an array or null
const safeValues = useMemo(() => values || [], [values]);
// ^ Use memo guards from reassigning the empty array
// on each render.
useEffect(function doSomethingWithValues() {
// Code fires when `value` changes.
}, [value])
return null;
}
☝️It's worth pointing out the previous solution with DEFAULT_VALUE
could work here as well. Choose whatever is suitable for your use case.
TL;DR; Not clearing side effects may lead to memory leaks, errors, and slow down the appliaction you're working on. E.g., firing event callbacks 10 times instead of just once (ouch!).
Watch out for events, promises, timers, or observers which can run after the effect has changed (due to its properties) OR the component holding it has been unmounted. It's important to realise useEffect's return value (and the function itself) fires each time one of the dependencies changes. Not only when component is mounted and unmounted.
Bad:
useEffect(function exampleEffect() {
const handleKeyUp = ({ altKey }) => {
if (altKey) someFunction();
}
document.addEventListener('keyup', handleKeyUp);
}, [someFunction]);
Good:
useEffect(function exampleEffect() {
const handleKeyUp = ({ altKey }) => {
if (altKey) someFunction();
}
document.addEventListener('keyup', handleKeyUp);
return () => {
document.removeEventListener('keyup', handleKeyUp);
};
}, [someFunction]);
Bad:
useEffect(function exampleEffect() {
setTimeout(() => setValueState(value), 2000);
}, [value]);
Good:
useEffect(function exampleEffect() {
const timerId = setTimeout(() => setValueState(value), 2000);
return () => {
clearTimerout(timerId);
};
}, [value]);
// ^ Aside from debouncing, clearing the timeout guards from
// setting the state on an unmounted component.
Promise based code is more complex and requires different strategies depending on the case. E.g., you may want to abort signal for a fetch request or it might be a better idea to ignore a promise. See: an effective pattern to deal with promises firing after unmounting the component. The link targets one specific section of a larger article. It's totally worth to read the whole piece though!
TL;DR; It's not something super important but can help you maintaining large components/custom hooks.
Named function increase the code readability. Aside from that, they output more accurate error messages. The named function is visible in the stack trace helping to quickly navigate to the source of the problem. It's especially valuable in case of complex components with many effects.
Example:
Bad:
useEffect(() => {
console.log('message');
}, []);
Good:
useEffect(function logMessage() {
console.log('message');
}, []);
TL;DR; Not adding them results in unpredictable code and can lead to nasty errors.
Many of the Hooks coming with React require a dependency array: useEffect
, useLayoutEffect
, useMemo
, useCallback
. The purpose of this array is to keep refreshing the function inserted in each of those hooks according to the changing dependencies so that it always has access to the up to date values.
Quick example:
Bad:
const Component = () => {
const [val, setVal] = React.useState(0);
const alertValue = React.useCallback(() => {
alert(val);
}, []); // <- Missing `val` in the dependencies. ❌
// ^ The function will always display an alert with `0`.
return (
<>
<button type="button" onClick={() => setVal((prev) => prev 1)}>
Increase Val
</button>
<button type="button" onClick={alertValue}>
Display Val
</button>
</>
);
};
Good:
const Component = () => {
const [val, setVal] = React.useState(0);
const alertValue = React.useCallback(() => {
alert(val);
}, [val]); // <- `val` present. ✅
// ^ The function will display an alert with the current `val`.
return (
<>
<button type="button" onClick={() => setVal((prev) => prev 1)}>
Increase Val
</button>
<button type="button" onClick={alertValue}>
Display Val
</button>
</>
);
};
-
You're going to stumble upon much more complicated cases when working on real world applications. However, skipping the dependencies is hardly ever a good idea.
-
You might be under impression omitting dependencies might increase the overall performance but more often it can lead to all kind of weird, hard to track errors.
-
If you're looking for a quality article shedding more light on hook's dependencies then look no further: https://overreacted.io/a-complete-guide-to-useeffect/ This article feels more like a small book but it's well worth your time. It focuses on one
useEffect
but, even despite that, serves as a thorough overview of the mechanics behind hooks.