React Hooks were introduced in version 16.8.0 as function component counterpart of class component lifecycles. For more information, see the official React Hooks API Reference.
Hooks flow
Before starting, it is important to understand the flow of react hooks.
Here is a diagram that explains it visually:
Note: The most important thing here is to notice how the "Run Effects" phase is executed last.
Here is a code example that explains this flow:
export const MyComponent: FC = () => {
// 1. Run Lazy initializers (i.e. () => 0)
const [state, setState] = useState(() => 0);
const previousStateRef = useRef();
useEffect(() => {
// 3. Run effect
previousStateRef.current = state;
}); // no deps array, because we want this to be called on every render
// 2. Render
return (
<>
Prev: {previousStateRef.current ?? "undefined"}
<br />
Current: {state}
<br />
<button onClick={() => setState(1)}>Update</button>
</>
);
}
In the "Mount" phase you will see:
Prev: undefined
Current: 0
This is because useEffect
is called after the first render and because assigning a value to ref
does not trigger a re-render - instead, the value is populated and waiting for the next update phase.
When button
is clicked, React will trigger the update
phase and the result will be:
Prev: 0
Current: 1
You can try this example out in codesandbox
:
useEffect
Using hook dependency arrays in Dependency arrays are a way to trigger re-firing of hooks only when their dependencies change. React will keep the values generated by a hook stored in memory and then run it when a change of a dependency has occurred. This applies to all hooks that can have dependencies.
export const MyComponent: FC = ({ numberProp, stringProps }) => {
useEffect(() => {
// Runs only when `numberProps` or `stringProp` changes
}, [numberProp, stringProp])
// ...
}
For non-primitive values like objects, arrays and functions, React will do a reference comparison using Object.is(). This means that we must provide the same reference of objectProp
, arrayProp
, functionProp
to Child
on every re-render if we don't want to trigger useEffect
every time. This can be achieved by extracting them outside of the component or wrapping them in useMemo
or useCallback
.
Check Using Object.is section on MDN for better understanding.
Note: There is a JavaScript Records & Tuples Proposal which will invalidate above arguments and make things simpler.
const Child: FC = ({ objectProp, arrayProp, functionProp }) => {
useEffect(() => {
// Runs only when `objectProp`, `arrayProp` or `functionProp` reference changes
}, [objectProp, arrayProp, functionProp])
return ...;
}
// BAD
const Parent: FC = () => {
// here we could have some code unrelated to Child component that could trigger re-render of Parent
return (
<Child
objectProp={{ a: 'a' }}
arrayProp={['a']}
functionProp={() => 'a'}
/>
// some other components...
);
}
// GOOD
const objectProp = { a: 'a' };
const arrayProp = ['a'];
const Parent: FC = () => {
// here we could have some code unrelated to Child component that could trigger re-render of Parent
const functionProp = useCallback(() => 'a', []);
return (
<Child
objectProp={objectProp}
arrayProp={arrayProp}
functionProp={functionProp}
/>
// some other components...
);
}
// FUTURE with Records & Tuples
const Parent: FC = () => {
const functionProp = useCallback(() => 'a', []);
return (
<Child
objectProp={#{ a: 'a' }}
arrayProp={#['a']}
functionProp={functionProp}
/>
);
}
Use an empty dependency array if you want a hook to fire only on initial render.
export const MyComponent: FC = () => {
useEffect(() => {
// Runs only on initial render
}, [])
// ...
}
If you don't provide a dependency array at all, useEffect
will be called on mount
and on each update
.
For example, this could be used for storing previous values or sending events to an analytics service on each state change.
export const MyComponent: FC = () => {
const [state, setState] = useState(() => 0);
const prevRef = useRef();
useEffect(() => {
// runs on mount and on each update
prevRef.current = state;
})
// ...
}
You may think to yourself: "Why can't we just do it without useEffect
?. What's the difference?".
There is an upcoming React feature called Concurrent Mode, so we can consider this approach as kind of a preparation for that feature. So, we don't want to call a function directly because it could slow down or even prevent things from rendering. We want to offload the Side Effects somewhere else and leave the "main thread" intact and ready to execute.
Useful links: 1. A Complete Guide to useEffect
Avoid misusing hook dependencies
As all values and references are recreated on each render, if you use anything from inside the component scope, be sure to provide it as a dependency, even if you think that the dependency will never change.
For example, when a useEffect
includes a prop or a value created inside the component's scope, you might be tempted to pass an empty array as a dependency to run it only on the initial render (trying to use it as you would componentDidMount
in class components):
// BAD
export const MyComponent: FC = () => {
const [value, setValue] = useState();
const updateState = () => {
// Do something with `setValue`
}
useEffect(() => {
updateState();
}, [])
// ...
}
// BAD
export const MyComponent: FC = ({ handlerFunction }) => {
useEffect(() => {
handlerFunction();
}, [])
// ...
}
Or you might want to skip some of the dependencies that you think will not change:
// BAD
const MyComponent: FC = () => {
const [value, setValue] = useState(null);
const doSomethingOnValueChange = () => {...}
useEffect(() => {
if (value) {
doSomethingOnValueChange(numberProp);
}
}, [value]) // missing a dependency (`doSomethingOnValueChange`)
// ...
}
While you might think that a value will never change, it is not guaranteed, and React could use a stale value or reference, which might introduce bugs that are hard to trace in the future.
If you are doing this, there is a good chance that you should rethink the implementation and you actually need an alternative approach.
Derive new state from the previous state value, if possible
This way, you can avoid using state values as hook dependencies and causing additional re-renders.
// BAD
const Counter: FC = ({ incrementBy = 1 }) => {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount(count + incrementBy), [
incrementBy,
count,
])
// ...
}
// GOOD
const Counter: FC = ({ incrementBy = 1 }) => {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount((previousCount) => previousCount + incrementBy), [incrementBy])
// ...
}
Using lazy initialization when setting initial state with functions
If you are setting state with a function call, use lazy initialization and wrap it in an arrow function. This way you ensure that the function will be invoked only on first render:
// BAD
const [id, setId] = useState(generateId());
// GOOD
const [id, setId] = useState(() => generateId());
If the initial state is undefined, specify the state type:
// BAD
const [text, setText] = useState();
// GOOD
const [text, setText] = useState<string>();
Declare functions outside of the component, when they don't use anything from component scope (if you need to optimize rendering)
const onButtonClick = (e: SyntheticEvent<HTMLButtonElement>) => {
// This event handler is using only DOM's `event`
}
const Button: FC = () => {
// ...
}
const changeDocumentTitle = (title) => {
document.title = title;
}
const App: FC = () => {
// ...
}
useEffect
Add event listeners in If you need to add event listeners from component scope, you can do it inside useEffect
. Don't forget to cleanup in the return statement, which will execute before component is unmounted.
const App: FC = () => {
useEffect(() => {
const handler = () => ...;
document.addEventListener('fullscreenchange', handler);
return () => {
document.removeEventListener('fullscreenchange', handler);
}
}, []);
// ...
}
Note: cleanup
is always a function.
useMemo
Wrap values derived from expensive calculations with If you are using an expensive calculation to assign a value and it is causing slower performance, you can utilize useMemo
to trigger that calculation only when necessary.
// BAD
export const MyComponent: FC = ({ propA, propB }) => {
const value = someExpensiveCalculation(propA, propB);
// ...
}
// GOOD
export const MyComponent: FC = ({ propA, propB }) => {
const memoizedValue = useMemo(() => someExpensiveCalculation(propA, propB), [propA, propB]);
// ...
}
Defer creation of non-primitive values if you are using them within a dependency array
Since objects will be recreated on each render, they will also have a different reference every time - and useEffect
will treat it as a changed dependency.
// BAD
export const MyComponent: FC = ({ propA, propB }) => {
const shape = {
a: propA,
b: propB,
};
useEffect(() => {
/**
* On each render, `useEffect` is causing an additional re-render,
* since it's dependency is an object with a new reference every time.
*/
doSomeSideEffectsWithShape(shape);
}, [shape])
// ...
}
Assuming that propA
and propB
are primitive (or memoized, non-primitive) values, with useMemo
you are ensuring that non-primitive references are retained throughout re-renders, until propA
or propB
have changed.
export const MyComponent: FC = ({ propA, propB }) => {
useEffect(() => {
// Triggers re-renders only if propA and propB have changed
const shape = {
a: propA,
b: propB,
};
doSomeSideEffectsWithShape(shape);
}, [propA, propB])
// ...
}
Wrap non-primitive values with useMemo, if you are sending them as props
You can use useMemo
if you need to pass an non-primitive value as a prop to a component and you know it will cause a large subtree to re-render each time it changes:
export const MyDataChartWithIntersection: FC = ({ xAxisData, yAxisData }) => {
// getChartData is expensive calculation
const data = useMemo(() => getChartData(xAxisData, yAxisData), [xAxisData, yAxisData]);
// getIntersections is expensive calculation
const intersection = useMemo(() => getIntersections(xAxisData, yAxisData), [xAxisData, yAxisData])
return (
<Chart data={data} intersection={intersection}/>
);
}
useMemo
as a semantic guarantee that it will be a constant throughout component re-renders
Do not use If you need a value to stay the same throughout re-renders, you might think of useMemo
as a nice way to "mimic" a constant. While it might seem that way, it is not guaranteed.
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for off-screen components. Write your code so that it still works without useMemo — and then add it to optimize performance when it's necessary.
To see what the alternatives are, check out the keeping consistent values trough rerenders recipe.
Two-pass rendering
If you intentionally need to render something different on the server or on the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like isClient
, which you can set to true in useEffect
(since useEffect
only runs on the client). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.
Remember to be mindful of user experience on slow connections. The JavaScript code may load significantly later than the initial HTML render, so if you render something different in the client-only pass, the transition can be jarring. However, if executed well, it may be beneficial to render a “shell” of the application on the server, and only show some of the extra widgets on the client. To learn how to do this without getting the markup mismatch issues, refer to the explanation in the imperative update recipe.
const useIsClient = () => {
const [isClient, setClient] = useState(false);
useEffect(() => {
setClient(true)
}, []);
return isClient;
}
export const Component: FC = () => {
const isClient = useIsClient();
return <div>{isClient ? 'Client' : 'Server'}</div>
}
useCallback
if they can cause large subtrees to re-render too often
Wrap functions with While it is mostly unnecessary to wrap functions with useCallback
, since the memoization process will usually be as expensive or more, it can be useful if functions' dependencies change often or if they are passed down to a large subtree of children.
// In this case, memoization is probably not worth it, since it will not necessarily improve performance
export const MyButton: FC = () => {
const onClick = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
...
}, [])
return (
<Button onClick={onClick} />
);
}
// Here, memoization is probably useful, because `onClick` will stay the same if other state changes re-render the component, and will not trigger a large subtree to re-render unnecessarily
export const MyList: FC = () => {
// ...
const onListItemClick = useCallback(() => {
...
}, [...])
return <LargeList onListItemClick={onListItemClick} />;
}
You don't have to avoid "inlining" non-expensive functions
React is good at optimizing, so if you prematurely decide to wrap a function inside a useCallback
, often you will end up with slower performance. This is because memoization is expensive. In most cases, even if you improve performance, it will be an insignificant gain compared to simple and maintainable code you had before.
export const StepButton: FC = ({ onClick, stepSize }) => {
/**
* You don't have to optimise (wrap in useCallback) because onClick and stepSize
* are the same on each render and React can optimise this by itself.
*/
return <button onClick={() => onClick(stepSize)}>Increment for {stepSize}</button>;
}
// GOOD
export const MyComponent: FC = () => {
const [count, setCount] = useState(0);
const increment = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);
return (
<div>
<div>Count {count}</div>
<StepButton onClick={increment} stepSize={2} />
</div>
);
}
Hooks encapsulation
A common problem with hooks is that the components using them can go out of control and become unreadable and messy.
This happens if you have multiple invocations of useEffect
and useCallback
in your function component body, which happens often if you are building real world products. To overcome this problem we can do the same thing as we would do in class components - split things into smaller chunks of logic and extract them, i.e. private methods of class components or custom hooks in function components.
The Problem:
This is a continuation of the previous section, but we will expand on it with some additional requirements. We need to add an input field for setting the description of the value that we are counting, with decrement and reset handlers for the counter.
This is the previous code:
export const MyComponent: FC = () => {
const [count, setCount] = useState(0);
const increment = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);
return (
<div>
<div>Count {count}</div>
<StepButton onClick={increment} stepSize={2} />
</div>
);
}
This is the updated code with additional features.
export const MyComponent: FC = () => {
const [count, setCount] = useState(0);
const [inputState, setInputState] = useState('')
const increment = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);
const decrement = useCallback((stepSize) => { setCount((previousCount) => previousCount + stepSize) }, []);
const reset = useCallback(() => { setCount(0) }, []);
const handleInputChange = useCallback(e => {
setInputState(e.target.value);
}, []);
return (
<div>
<div>
<label htmlFor="entity">Entity</label>
<input
type="text"
id="entity"
onChange={handleInputChange}
value={inputState}
/>
</div>
<div>Count {count}</div>
<StepButton onClick={increment} stepSize={2} />
<StepButton onClick={decrement} stepSize={-2} />
<button onClick={reset}>Reset</button>
</div>
);
}
You can see that our component becomes messy and hard to understand. There is a lot of code in the body of the function which increases our cognitive load. To fix this issue we can encapsulate our elements of concern into a separate chunks of work, i.e. custom hooks.
The Solution:
const useCount = (initialState = 0) => {
const [state, setState] = useState(() => initialState);
const handlers = useMemo(
() => ({
increment: (stepSize = 1) => {
setState(previousCount => previousCount + stepSize)
},
decrement: (stepSize = -1) => {
setState((previousCount) => previousCount + stepSize)
},
reset: () => {
setState(0)
}
}),
[]
);
return [state, handlers];
}
const useInput = (initialState = '') => {
const [state, setState] = useState(() => initialState);
const handlers = useMemo(() => ({
handleInputChange: (event) => {
setState(event.target.value);
}
})), []);
return [state, handlers]
}
export const MyComponent: FC = () => {
const [count, { increment, decrement, reset }] = useCount();
const [inputState, { handleInputChange }] = useInput();
return (
<div>
<div>
<label htmlFor="entity">Entity</label>
<input
type="text"
id="entity"
onChange={handleInputChange}
value={inputState}
/>
</div>
<div>Count {count}</div>
<StepButton onClick={increment} stepSize={2} />
<StepButton onClick={decrement} stepSize={-2} />
<button onClick={reset}>Reset</button>
</div>
);
Useful links: 1. useEncapsulation or Why Your React Components Should Only Use Custom Hooks 2. Encapsulation or the Primary Purpose of Functions
Before you use memoization
Have in mind that React is really good at optimizing re-renders by default.
You might get tempted to wrap values and functions with useMemo
and useCallback
all the time, but in many of these cases, you don't really need it, and you might even make your app performance and file size worse. These calculations can be expensive and you could end up using more memory than you would without them and make your code more complicated to read and maintain.
If it's not obvious that memoization is needed, profile your app performance without it first, using React Devtools, and then optimize if necessary.
⚛️🛠 Prototype of a new Profiler feature, "Scheduled by", enumerating which fibers triggered the current commit (which ones called set state).
— Brian Vaughn 🖤 (@brian_d_vaughn) May 10, 2019
Would this be useful? Could it be more useful? pic.twitter.com/7AvVHB0wPY
If you want to dive deeper here are some useful articles: 1. When to useMemo and useCallback 2. One simple trick to optimize React re-renders 3. Profile a React App for Performance 4. React Production Performance Monitoring