Rooks

useSafeSetState

About

A hook that provides a safe version of useState that prevents state updates after the component has unmounted. This is particularly useful for async operations to avoid memory leaks and React warnings.

Examples

Basic example

import { useSafeSetState } from "rooks";
 
export default function App() {
  const [count, setSafeCount] = useSafeSetState(0);
 
  const handleAsyncIncrement = async () => {
    // Simulate an async operation
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // This will only update state if the component is still mounted
    setSafeCount(prev => prev + 1);
  };
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleAsyncIncrement}>
        Async Increment (2s delay)
      </button>
      <button onClick={() => setSafeCount(0)}>
        Reset
      </button>
    </div>
  );
}

Example with fetch operation

import { useSafeSetState } from "rooks";
import { useEffect } from "react";
 
export default function UserProfile({ userId }) {
  const [user, setSafeUser] = useSafeSetState(null);
  const [loading, setSafeLoading] = useSafeSetState(true);
 
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setSafeLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        
        // Safe to call even if component unmounts during fetch
        setSafeUser(userData);
        setSafeLoading(false);
      } catch (error) {
        console.error('Failed to fetch user:', error);
        setSafeLoading(false);
      }
    };
 
    fetchUser();
  }, [userId, setSafeUser, setSafeLoading]);
 
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

Example with functional state updates

import { useSafeSetState } from "rooks";
 
export default function Counter() {
  const [state, setSafeState] = useSafeSetState({
    count: 0,
    lastUpdated: Date.now()
  });
 
  const increment = () => {
    setSafeState(prevState => ({
      count: prevState.count + 1,
      lastUpdated: Date.now()
    }));
  };
 
  const delayedIncrement = () => {
    setTimeout(() => {
      // This update will be ignored if component unmounts
      setSafeState(prevState => ({
        ...prevState,
        count: prevState.count + 1
      }));
    }, 3000);
  };
 
  return (
    <div>
      <p>Count: {state.count}</p>
      <p>Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}</p>
      <button onClick={increment}>Increment Now</button>
      <button onClick={delayedIncrement}>Increment in 3s</button>
    </div>
  );
}

Arguments

Argument valueTypeDescriptionDefault
initialStateTThe initial state valueundefined

Returns

Returns an array with two elements:

Return valueTypeDescriptionDefault
stateTThe current state valueundefined
safeSetStateDispatch<SetStateAction<T>>Function to update state, but only if component is still mountedundefined

Behavior

The useSafeSetState hook behaves identically to React's built-in useState hook with one important difference: the setter function (safeSetState) will only update the state if the component is still mounted.

This prevents:

  • Memory leaks from async operations
  • React warnings about setting state on unmounted components
  • Unnecessary state updates that won't affect the UI

The hook internally uses useGetIsMounted to check if the component is still mounted before allowing state updates.

Use Cases

  • Async API calls: Prevent state updates from completed requests after navigation
  • Timers and intervals: Avoid state updates from delayed operations
  • Event handlers: Safe state updates in long-running event callbacks
  • Cleanup-sensitive operations: Any operation that might complete after component unmount

On this page