useSafeSetState
Calls setState only if the component is still mounted, preventing memory leaks.
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>
);
}Parameters
| Argument value | Type | Description | Default |
|---|---|---|---|
| initialState | T | Required initial state value | — |
Return value
Returns an array with two elements:
| Return value | Type | Description | Default |
|---|---|---|---|
| state | T | The current state value | — |
| safeSetState | Dispatch<SetStateAction<T>> | Updates state only while the component is mounted | — |
Behavior and lifecycle
The setter first calls the stable mounted-state reader from useGetIsMounted. It forwards the value or functional updater to React only while mounted; after the layout-effect cleanup marks the component unmounted, calls are ignored.
It does not cancel a request, timer, subscription, or other asynchronous work. Clean those resources up independently when possible; this hook only prevents their late completion callbacks from changing state.
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
Compatibility and accessibility
The state logic itself is platform-independent and SSR-safe. As with useState, accessibility depends on the controls and status UI built from the value.
Related
- useGetIsMounted exposes the mounted-state reader directly.