The Hidden Performance Costs of React's useEffect

Is useEffect slowing down your React app? Learn about common performance pitfalls and how to optimize your effects for better performance.

Think your React app is slow? Your useEffect hooks might be the culprit. Here's what no one tells you about useEffect performance.


Common Mistakes

1. Unnecessary Dependencies

// ❌ BAD: Over-dependent effect
const UserProfile = ({ userId }: Props) => {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId, fetchUser]); // fetchUser shouldn't be here
  
  return <div>{user?.name}</div>;
};

// ✅ BETTER: Stable dependencies
const UserProfile = ({ userId }: Props) => {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // Only depend on what changes
  
  return <div>{user?.name}</div>;
};

2. Chain of Effects

// ❌ BAD: Effect chain
const Dashboard = () => {
  const [user, setUser] = useState<User | null>(null);
  const [posts, setPosts] = useState<Post[]>([]);
  const [comments, setComments] = useState<Comment[]>([]);

  useEffect(() => {
    fetchUser().then(setUser);
  }, []);

  useEffect(() => {
    if (user) {
      fetchPosts(user.id).then(setPosts);
    }
  }, [user]);

  useEffect(() => {
    if (posts.length) {
      fetchComments(posts).then(setComments);
    }
  }, [posts]);

  // Each effect triggers a re-render!
};

// ✅ BETTER: Combined data fetching
const Dashboard = () => {
  const [data, setData] = useState<DashboardData | null>(null);

  useEffect(() => {
    async function loadDashboard() {
      const user = await fetchUser();
      const posts = await fetchPosts(user.id);
      const comments = await fetchComments(posts);
      setData({ user, posts, comments });
    }
    loadDashboard();
  }, []);
};

Performance Optimizations

1. Debounced Effects

// ❌ BAD: Frequent API calls
const SearchComponent = ({ query }: Props) => {
  const [results, setResults] = useState([]);

  useEffect(() => {
    searchApi(query).then(setResults);
  }, [query]); // Fires on every keystroke!
};

// ✅ BETTER: Debounced effect
const SearchComponent = ({ query }: Props) => {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const handler = setTimeout(() => {
      searchApi(query).then(setResults);
    }, 300);

    return () => clearTimeout(handler);
  }, [query]); // Only fires after typing stops
};

2. Memoized Values

// ❌ BAD: Recreating objects in effect
const UserList = ({ users }: Props) => {
  useEffect(() => {
    const userMap = users.reduce((acc, user) => ({
      ...acc,
      [user.id]: user
    }), {});
    // This object is recreated every render
  }, [users]);
};

// ✅ BETTER: Memoized computation
const UserList = ({ users }: Props) => {
  const userMap = useMemo(() => {
    return users.reduce((acc, user) => ({
      ...acc,
      [user.id]: user
    }), {});
  }, [users]);

  useEffect(() => {
    // Use stable userMap
  }, [userMap]);
};

Real World Patterns

1. Resource Cleanup

// ❌ BAD: Missing cleanup
const ChatRoom = ({ roomId }: Props) => {
  useEffect(() => {
    const socket = connectToRoom(roomId);
    socket.on('message', handleMessage);
  }, [roomId]); // Memory leak!
};

// ✅ BETTER: Proper cleanup
const ChatRoom = ({ roomId }: Props) => {
  useEffect(() => {
    const socket = connectToRoom(roomId);
    socket.on('message', handleMessage);

    return () => {
      socket.disconnect();
      socket.off('message', handleMessage);
    };
  }, [roomId]);
};

2. Data Synchronization

// ❌ BAD: Multiple sync effects
const UserSettings = ({ userId }: Props) => {
  useEffect(() => {
    syncPreferences(userId);
  }, [userId]);

  useEffect(() => {
    syncNotifications(userId);
  }, [userId]);

  useEffect(() => {
    syncTheme(userId);
  }, [userId]);
};

// ✅ BETTER: Batched synchronization
const UserSettings = ({ userId }: Props) => {
  useEffect(() => {
    const sync = async () => {
      await Promise.all([
        syncPreferences(userId),
        syncNotifications(userId),
        syncTheme(userId)
      ]);
    };
    sync();
  }, [userId]);
};

Measuring Impact

1. Performance Monitoring

const useEffectWithLogging = (
  effect: EffectCallback,
  deps: DependencyList,
  name: string
) => {
  useEffect(() => {
    const start = performance.now();
    const cleanup = effect();
    const duration = performance.now() - start;
    
    console.log(`Effect ${name} took ${duration}ms`);
    
    return cleanup;
  }, deps);
};

2. Render Counting

const useRenderCount = (componentName: string) => {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    console.log(`${componentName} rendered ${renderCount.current} times`);
  });
};

Best Practices

1. Effect Organization

// Separate concerns
const UserDashboard = () => {
  // Data fetching effect
  useEffect(() => {
    fetchUserData();
  }, []);

  // Event listeners
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  // Subscriptions
  useEffect(() => {
    const subscription = subscribe();
    return () => subscription.unsubscribe();
  }, []);
};

2. Custom Hooks

// Encapsulate effect logic
const useUserData = (userId: string) => {
  const [data, setData] = useState<UserData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let mounted = true;

    const loadUser = async () => {
      try {
        const userData = await fetchUser(userId);
        if (mounted) {
          setData(userData);
          setLoading(false);
        }
      } catch (err) {
        if (mounted) {
          setError(err);
          setLoading(false);
        }
      }
    };

    loadUser();

    return () => {
      mounted = false;
    };
  }, [userId]);

  return { data, loading, error };
};

Conclusion

Optimize your useEffect usage by:

  • Minimizing dependencies
  • Combining related effects
  • Proper cleanup
  • Using custom hooks
  • Performance monitoring
  • Debouncing when needed

Remember: The best effect is often no effect at all.