Introduction
React has rapidly become one of the most popular frameworks for building web applications thanks to its component-based architecture and efficient DOM rendering through the virtual DOM. As React applications grow in complexity, developers need to optimize performance and prevent bugs using some of its advanced hooks like useref, useEffect, and useCallback.
In this article, we will demystify these APIs and explain how and when to use them through concrete examples. Understanding these fundamental concepts will level up your React skills and help you build robust web apps. Let’s get started!
useref hook in React
The useref
hook in React allows you to persist values between renders and reference them manually. This can be useful for keeping previous state to compare against during re-renders or for performance optimizations.
For example, let’s say we want to only run an expensive calculation if some dependencies have changed:
function MyComponent() {
const previousVal = useRef();
function calculateExpensiveValue(deps) {
const expensive = runComplexCalculation(deps);
if (previousVal.current !== expensive) {
previousVal.current = expensive;
}
return previousVal.current;
}
return <div>{calculateExpensiveValue(deps)}</div>
}
We store the previous result in a ref
created with useRef()
and only recalculate if the value changes. This avoids unnecessary recalculations on every render.
Some other common uses of useref
:
- Access DOM elements directly for manipulation
- Keep any mutable value around similar to an instance variable
- Trigger imperative actions on components
- Hold setTimeout/setInterval IDs for cleanup
In summary, useref
provides access to a mutable variable that persists between renders. This enables referencing values from previous renders or imperatively changing state.
useEffect hook in React
The useEffect
hook allows you to handle side effects in your components. Some examples of side effects are fetching data, directly updating the DOM, and timers.
Using useEffect
ensures that your effect callback gets cleaned up when the component unmounts:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// Clean up
subscription.unsubscribe();
};
});
We return a cleanup function from our effect to prevent memory leaks. By default, effects run after every render but you can control when it runs by passing a second argument:
useEffect(() => {
// Run only on mount
}, []);
useEffect(() => {
// Run on updates to props.count
}, [props.count]);
This allows granular control over when effects execute. Overall useEffect
provides an essential way to manage state changes and deal with side effects in React components.
useCallback
The useCallback
hook allows you to memoize callback functions in React. This is useful for performance optimization in components that rely on reference equality to prevent unnecessary re-renders.
Consider this example:
function Parent({ numClicks }) {
const handleClick = () => {
// ...
}
return <Child onClick={handleClick} />
}
function Child({ onClick }) {
// ...
}
Here, a new handleClick
callback is created on each Parent
re-render. This will cause Child
to re-render unnecessarily even though the actual callback didn’t change.
We can optimize this with useCallback
:
const memoizedHandleClick = useCallback(
() => {
// ...
},
[], // Only recreate if dependencies change
);
return <Child onClick={memoizedHandleClick} />
Now Child
will only re-render if memoizedHandleClick
changes identity. In summary, useCallback
helps optimize performance and avoid unnecessary re-renders due to changed function references.
When to use useref vs useState vs useReducer?
useref
, useState
, and useReducer
all allow you to manage state in a React component. Here are some guidelines on when to use each:
- useState – For managing simple state like strings, numbers, booleans etc. Provides setState method to update state.
- useReducer – For complex state objects/arrays where updates depend on current state. Provides dispatch method.
- useref – For persisting values between renders that you want to mutate directly rather than via re-renders. Doesn’t notify listeners of changes.
So in summary:
useState
is the default for reactively tracking stateuseReducer
is good for complex state transitionsuseref
is useful for keeping mutable values around manually
Choose the right hook based on how state needs to be updated and consumed in your components.
Examples of commonly used hooks in React
Here are some of the most commonly used React hooks with examples:
useState hook in React
const [count, setCount] = useState(0); // Basic state variable
setCount(prevCount => prevCount + 1); // Function update form
useEffect hook in React
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run if count changes
useContext
const UserContext = React.createContext();
function Component() {
const user = useContext(UserContext); // Get context value
return <div>{user.name}</div>
}
useRef
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []); // Imperatively focus input
useCallback hook in React
const memoizedCallback = useCallback(
() => {
doSomething();
},
[],
); // Memoize callback
There are many other hooks like useReducer
, useMemo
, useImperativeHandle
etc that enrich component functionality. But these five form the core set of commonly used React hooks.
useEffect vs useLayoutEffect
Both useEffect
and useLayoutEffect
allow you to manage side effects in your components. The key difference is when they are executed:
useEffect
fires asynchronously after render is committed to screenuseLayoutEffect
fires synchronously after render but before screen paint
This makes useLayoutEffect
useful for reading DOM layout before updates or synchronously re-rendering. For example:
useLayoutEffect(() => {
// Measure a DOM node
});
useEffect(() => {
// Fetch data
});
Since useLayoutEffect
blocks visual updates, overusing it can negatively impact performance. The default useEffect
is recommended in most cases.
In summary, useLayoutEffect
runs earlier but blocks the UI so only use it for DOM mutations or critical rendering logic. Prefer useEffect
otherwise.
Common mistakes with useEffect
useEffect
is powerful but some common mistakes can lead to bugs if you’re not careful:
Forgetting to clean up
Effects should clean up after themselves to avoid memory leaks. For example, clearing timers, removing listeners, closing connections etc.
Not handling dependencies correctly
Specifying correct dependencies for the effect is important to avoid stale data or infinite loops.
Fetching data directly
Fetching data directly in useEffect
can lead to multiple requests. It’s better to declare data requirements and fetch it at a higher level component.
Updating state without functional updates
Updating state without using the updater function from useState
will not trigger a re-render.
Too many effects
Having many separate effects throughout components can make optimization difficult. Consolidate related logic into custom hooks for cleaner code.
Overall, explicitly handling dependencies, cleaning up effects, using state updater functions properly, and consolidating logic will help avoid many common useEffect
pitfalls.
Performance optimizations with React.memo and useCallback
Two useful techniques for optimizing React app performance are React.memo
and useCallback
.
React.memo
Wrapping a component in React.memo
will shallowly compare its props before re-rendering:
const MyComponent = React.memo(function MyComponent(props) {
// ...
});
This prevents unnecessary re-renders if the props are the same.
useCallback
Wrapping functions in useCallback
will return the same reference between re-renders:
const memoizedHandleClick = useCallback(() => {
// Do something
}, []);
return <MyComponent onClick={memoizedHandleClick} />
Now MyComponent
will not re-render if just memoizedHandleClick
changes.
Using React.memo
and useCallback
together optimizes components by preventing unnecessary re-renders. But use judiciously only after profiling to ensure over-optimization doesn’t occur.
Conclusion
useref, useEffect, and useCallback are key to building performant real-world applications with React. useref provides access to persistent mutable values. useEffect enables handling side effects declaratively. And useCallback allows optimizing referential equality.
There are nuances to each of these APIs that can take time to master. But learning these React hooks deeply will level up your skills and help avoid many common pitfalls. Understanding when and how to leverage each hook based on your specific component needs is crucial for app performance and correctness.
Frequently Asked Questions
Should all state be kept in useState?
Not necessarily. useState is useful for state that causes re-renders when updated. However, you can also use useRef for mutable state that you don’t want to trigger re-renders on each update.
How does useCallback prevent unnecessary re-renders?
useCallback returns the same callback reference between re-renders. This allows components like React.memo to know not to re-render if just the parent callback changes reference. It optimizes for referential equality.
When would you use useLayoutEffect over useEffect?
useLayoutEffect is useful for cases where you need to perform some action after render but before the browser paints updates to the screen. Common examples are reading DOM layout before rendering or synchronously re-rendering.
What’s the difference between useEffect dependencies and useCallback dependencies?
useEffect dependencies cause the effect callback to re-run after renders where those values change. useCallback dependencies determine when the memoized callback should be recreated.
How can useRef be used for animation?
useRef can be used to store a mutable ref to a DOM element that you want to imperatively animate. You can update styling directly on this element and avoid re-renders by not using useState.