Back to articles

Beyond useEffect: Mastering React's Effects with Signals

AuthorMajd Muhtaseb06/20/20257 minutes
Beyond useEffect: Mastering React's Effects with Signals

The Limitations of useEffect

useEffect is a cornerstone of React, allowing us to perform side effects – data fetching, DOM manipulation, subscriptions – within our components. However, it can sometimes lead to performance bottlenecks and complex code, especially when dealing with multiple dependencies and frequent updates. Unnecessary re-renders and the need for meticulous dependency array management are common pain points.

Enter React Signals

React Signals (available through libraries like @preact/signals-react and others) provide a more granular and reactive approach to managing state and side effects. They allow components to subscribe directly to specific data changes, triggering updates only when necessary.

A Simple Comparison

Let's consider a scenario where we need to log a value whenever it changes:

Using useEffect:

import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`Count updated: ${count}`);
  }, [count]); // Dependency array is crucial!

  return (
    <button onClick={() => setCount(count + 1)}>Increment</button>
  );
}

export default MyComponent;

Using React Signals:

import React from 'react';
import { useSignal } from '@preact/signals-react';

function MyComponent() {
  const count = useSignal(0);

  React.useEffect(() => {
    console.log(`Count updated: ${count.value}`); // Access value using .value
  }, [count]);

  return (
    <button onClick={() => count.value++}>Increment</button>
  );
}

export default MyComponent;

While this example might seem similar, the true power of signals becomes apparent in more complex scenarios.

Fine-Grained Control

Signals allow for more precise control over when effects are triggered. Imagine a scenario where you only want to trigger an effect when a specific property within a larger object changes. With useEffect, you'd need to carefully manage the dependency array. With signals, you can subscribe specifically to that property.

import React from 'react';
import { useSignal } from '@preact/signals-react';

function MyComponent() {
  const user = useSignal({ name: 'Alice', age: 30 });

  React.useEffect(() => {
    console.log(`User's age updated: ${user.value.age}`);
  }, [user.value.age]); // Only triggers when age changes!

  return (
    <div>
      <p>Name: {user.value.name}</p>
      <p>Age: {user.value.age}</p>
      <button onClick={() => user.value = {...user.value, age: user.value.age + 1}}>Increment Age</button>
      <button onClick={() => user.value = {...user.value, name: 'Bob'}}>Change Name</button>
    </div>
  );
}

export default MyComponent;

Changing the name no longer triggers the effect, demonstrating the granular control signals provide.

Benefits of Using Signals for Effects

  • Improved Performance: Less unnecessary re-renders.
  • Simplified Code: Reduces the complexity of dependency arrays.
  • Fine-Grained Control: Subscribes to specific data changes.
  • Enhanced Reactivity: Provides a more reactive and efficient way to manage state and side effects.

Conclusion

React Signals offer a compelling alternative to useEffect for managing side effects, especially in complex applications. By providing fine-grained control and improved performance, they can lead to more maintainable and efficient React code. While useEffect remains a fundamental part of React, exploring signals is a valuable step towards mastering modern React development.