Back to articles

Beyond useEffect: Mastering React Side Effects with Signals

AuthorMajd Muhtaseb06/22/20257 minutes
Beyond useEffect: Mastering React Side Effects with Signals

The Problem with useEffect

useEffect is a cornerstone of React for managing side effects. However, complex dependencies and the potential for unnecessary re-renders can make it difficult to use effectively, leading to performance bottlenecks and confusing code. Consider this common scenario:

import { useState, useEffect } from 'react';

function MyComponent({ userId }) {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUserData(data);
    }

    fetchData();
  }, [userId]);

  return (
    <div>
      {userData ? <p>User Name: {userData.name}</p> : <p>Loading...</p>}
    </div>
  );
}

While functional, this useEffect hook re-fetches user data every time userId changes. For more complex scenarios with multiple dependencies, useEffect logic becomes harder to manage and debug.

Enter Signals: A Simpler Approach

Signals offer a reactive approach to state management, enabling fine-grained updates and simplified side effect handling. Libraries like @preact/signals-react provide seamless integration with React.

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

const userIdSignal = signal(1); //Initial user ID

function MyComponent() {
  const userData = useSignal(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch(`/api/users/${userIdSignal.value}`);
      const data = await response.json();
      userData.value = data;
    }

    fetchData();
  }, []); // No dependencies needed!

  return (
    <div>
      {userData.value ? <p>User Name: {userData.value.name}</p> : <p>Loading...</p>}
    </div>
  );
}

Key Advantages:

  • Direct Dependency Tracking: Signals automatically track which components and effects depend on them. Changes trigger only the necessary updates. In the above example, only when userIdSignal.value is changed the data will get fetched again.
  • Simplified useEffect: We don't need to list dependencies in useEffect. The signal manages the dependency tracking implicitly.

To change the userId and trigger a re-fetch, simply update the signal:

userIdSignal.value = 2; // Triggers re-fetch in MyComponent

Derived Signals and Computed Values

Signals also allow you to create derived signals or computed values that automatically update based on changes in other signals:

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

const firstName = signal("John");
const lastName = signal("Doe");

const fullName = computed(() => `${firstName.value} ${lastName.value}`);

console.log(fullName.value); // Output: John Doe

firstName.value = "Jane";

console.log(fullName.value); // Output: Jane Doe (automatically updated)

Conclusion

Signals offer a powerful alternative to useEffect for managing side effects in React. By embracing reactive principles, you can write cleaner, more performant, and easier-to-maintain code. Consider exploring signal libraries like @preact/signals-react to unlock the benefits of reactive state management in your next React project.