useRef, useEffect, and useCallback in React: A Comprehensive Guide

useRef, useEffect, and useCallback in React A Comprehensive Guide

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 state
  • useReducer is good for complex state transitions
  • useref 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 screen
  • useLayoutEffect 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.

Leave a Reply

Your email address will not be published. Required fields are marked *