Event Listener vs useState: A Performance Perspective

Event Listener vs useState: A Performance Perspective

How to optimize React apps by using event listeners in useEffect instead of frequent state updates. Improve performance and avoid unnecessary re-renders!

React’s useEffect hook is a powerful tool for handling side effects, such as interacting with external APIs, subscriptions, or DOM manipulations. However, how you use useEffect can significantly impact the performance of your application. A common performance pitfall occurs when developers overuse state updates for tasks that could instead leverage event listeners.

In this post, we’ll explore why using event listeners directly can sometimes be a more efficient approach compared to relying on React state, especially for frequent updates. We’ll walk through two approaches and analyze their impact on performance.


Why Updating State in useEffect Can Be Costly

When useEffect updates state, it triggers a component re-render. While re-renders are fundamental to React’s design, frequent updates can lead to:

  1. Excessive Re-renders: Unnecessary re-renders propagate to child components, increasing rendering costs.
  2. Performance Bottlenecks: Components reprocess logic when state updates aren’t critical for UI changes.
  3. UI Lag: Frequent updates, like tracking mouse movement, can degrade the responsiveness of your application.

Event Listeners to the Rescue

Event listeners allow you to interact directly with the DOM without relying on React state updates. This can significantly improve performance for tasks like mouse tracking or scroll events, where frequent updates are needed.


Example: Tracking Mouse Movement

We’ll compare two approaches for moving an element horizontally based on the mouse’s X position:

  1. Using useState to update the position.
  2. Using an event listener and useRef for direct DOM manipulation.

Approach 1: Updating State in useEffect

"use client"

import { FC, useEffect, useState } from "react"

const MouseTrackerWithState: FC = () => {
  const [mouseX, setMouseX] = useState(0)

  useEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      setMouseX(event.clientX)
    }

    window.addEventListener("mousemove", handleMouseMove)

    return () => {
      window.removeEventListener("mousemove", handleMouseMove)
    }
  }, [])

  return (
    <div style={{ marginLeft: `${mouseX}px` }}>
      Hello World
    </div>
  )
}

export default MouseTrackerWithState

Issues with This Approach:

  • Re-rendering: Every mousemove event triggers a setState, causing the component to re-render.
  • Performance Overhead: In scenarios with frequent updates, such as mouse tracking, state updates become costly, leading to laggy UI performance.

Approach 2: Using Event Listener and useRef

"use client"

import { FC, useEffect, useRef } from "react"

const MouseTrackerWithListener: FC = () => {
  const selfRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      if (!selfRef.current) return
      selfRef.current.style.marginLeft = `${event.clientX}px`
    }

    window.addEventListener("mousemove", handleMouseMove)

    return () => {
      window.removeEventListener("mousemove", handleMouseMove)
    }
  }, [])

  return (
    <div ref={selfRef}>
      Hello World
    </div>
  )
}

export default MouseTrackerWithListener

Benefits of This Approach:

  • No Re-renders: By directly manipulating the DOM element’s style, you avoid triggering React re-renders altogether.
  • Improved Performance: This method is far more efficient for frequent updates like mouse movements because React state isn’t involved.

Performance Comparison

MetricuseState ApproachEvent Listener with useRef
Re-rendersFrequent on every updateNone
React State OverheadYesNo
Performance EfficiencyLaggy under frequent updatesSmooth and responsive

When to Use Event Listeners Over State

Event listeners are ideal when:

  1. Frequent Updates Are Needed: For tasks like mouse tracking, scroll monitoring, or resizing.
  2. State Isn’t Required for UI Rendering: If the value only modifies the DOM without affecting React’s state logic.
  3. Performance Is Critical: For high-frequency updates, avoiding unnecessary re-renders ensures a smooth user experience.

Best Practices for Event Listeners in useEffect

  1. Use useRef for mutable values that don’t require re-renders.
  2. Always Cleanup: Remove event listeners in the useEffect cleanup function to prevent memory leaks.
  3. Throttle or Debounce: For extremely high-frequency updates, consider throttling or debouncing the event listener logic.

Conclusion

While React’s state management is a powerful tool, it’s not always the most efficient choice for tasks requiring frequent updates. By directly using event listeners with useRef, you can avoid unnecessary re-renders and significantly improve performance.

For scenarios like mouse movement, scroll tracking, or window resizing, consider bypassing React state and manipulating the DOM directly. This approach ensures smoother interactions and a more responsive user experience.

Choose the right tool for the job, and optimize your React applications for performance wherever it matters most.