A React hook that works just like useEffect
, but performs a deep comparison of its dependencies, preventing unnecessary re-runs when object or array references change but their content remains the same.
React's built-in useEffect
hook performs a shallow comparison of its dependencies. This means if you pass an object or an array as a dependency, useEffect
will re-run the effect every time the object/array reference changes, even if its internal content is identical.
This can lead to:
-
Unnecessary re-renders and computations: Your effect might run more often than needed, impacting performance.
-
Infinite loops: If your effect updates state that causes a new object/array reference to be created, and that object/array is a dependency, you can easily end up in a loop.
useDeepCompareEffect
solves this by performing a deep comparison of your dependencies. It only re-runs your effect when the actual content of your object or array dependencies changes, not just their reference.
-
Deep Equality Check: Compares the actual values of objects and arrays, not just their references.
-
Circular Reference Handling: Safely handles objects with circular references to prevent infinite loops during comparison.
-
Specific Type Handling: Correctly compares
Date
objects by their time value andRegExp
objects by their source and flags. -
Function Reference Comparison: Like
useEffect
, functions are compared by reference, ensuring standard React behavior for callbacks. -
Type-Safe: Built with TypeScript for a robust development experience.
You can install @n0n3br/use-deep-compare-effect
using npm, pnpm, or yarn.
using npm npm install @n0n3br/use-deep-compare-effect
using pnpm pnpm install @n0n3br/use-deep-compare-effect
using yarn yarn add @n0n3br/use-deep-compare-effect
Using useDeepCompareEffect
is almost identical to using useEffect
. Just import it and use it in place of useEffect
.
import React, { useState } from "react";
import { useDeepCompareEffect } from "use-deep-compare-effect"; // Adjust path if not installed as a package
function MyComponent() {
const [settings, setSettings] = useState({
theme: "dark",
notifications: {
email: true,
sms: false,
},
tags: ["react", "hook"],
});
const [effectLog, setEffectLog] = useState<string[]>([]);
// This effect will only run when the DEEP content of 'settings' changes.
useDeepCompareEffect(() => {
const timestamp = new Date().toLocaleTimeString();
setEffectLog((prev) => [
...prev,
`useDeepCompareEffect ran at: ${timestamp} (Settings changed deeply)`,
]);
console.log("Deeply compared effect ran:", settings);
// Optional cleanup function
return () => {
console.log("Deeply compared effect cleanup");
};
}, [settings]); // Dependencies are objects/arrays that need deep comparison
// --- For comparison: A standard useEffect ---
// This effect would run every time 'settings' reference changes,
// even if its content is the same.
// useEffect(() => {
// console.log('Standard useEffect ran:', settings);
// }, [settings]);
const updateTheme = () => {
setSettings((prev) => ({
...prev,
theme: prev.theme === "dark" ? "light" : "dark",
}));
};
const toggleEmailNotifications = () => {
setSettings((prev) => ({
...prev,
notifications: {
...prev.notifications,
email: !prev.notifications.email,
},
}));
};
const addTag = () => {
setSettings((prev) => ({
...prev,
tags: [...prev.tags, `new-tag-${prev.tags.length + 1}`],
}));
};
const triggerShallowUpdate = () => {
// This creates a NEW settings object reference, but with the SAME DEEP content
// useDeepCompareEffect WILL NOT run.
// Standard useEffect WOULD run.
setSettings((prev) => ({ ...prev }));
};
return (
<div
style={{
padding: "20px",
border: "1px solid #ccc",
borderRadius: "8px",
}}>
<h2>My Component with Deep Compare Effect</h2>
<pre>{JSON.stringify(settings, null, 2)}</pre>
<div style={{ display: "flex", gap: "10px", marginTop: "10px" }}>
<button onClick={updateTheme}>Update Theme (Deep Change)</button>
<button onClick={toggleEmailNotifications}>
Toggle Email Notif. (Deep Change)
</button>
<button onClick={addTag}>Add Tag (Deep Change)</button>
<button onClick={triggerShallowUpdate}>
Trigger Shallow Update (No Deep Change)
</button>
</div>
<h3>Effect Log:</h3>
<div
style={{
border: "1px solid #eee",
padding: "10px",
maxHeight: "150px",
overflowY: "auto",
}}>
{effectLog.length === 0 ? (
<p>No effect runs yet...</p>
) : (
<ul>
{effectLog.map((log, index) => (
<li key={index}>{log}</li>
))}
</ul>
)}
</div>
</div>
);
}
export default MyComponent;
Contributions are welcome! If you find a bug or have an idea for an enhancement, please open an issue or submit a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.