Implementing useDeepCompareEffect in React with TypeScript
The standard useEffect hook in React triggers re-runs when any dependency changes, even if the values are deeply equal. This can lead to unnecessary re-renders and performance bottlenecks, especially when dealing with complex objects or arrays. This challenge asks you to implement a custom hook, useDeepCompareEffect, that only triggers its effect when dependencies have changed based on a deep comparison.
Problem Description
You are tasked with creating a useDeepCompareEffect hook in React using TypeScript. This hook should mimic the functionality of useEffect but with a crucial difference: it should only execute its effect if the dependencies have changed based on a deep comparison (using JSON.stringify for simplicity).
What needs to be achieved:
- Create a custom React hook named
useDeepCompareEffectthat accepts a callback function and an array of dependencies. - The hook should execute the callback function only when the dependencies have changed based on a deep comparison.
- The hook should handle the initial mount scenario correctly.
Key Requirements:
- The hook must be written in TypeScript.
- Deep comparison should be performed using
JSON.stringifyfor simplicity. While not the most performant deep comparison method, it's sufficient for this exercise. - The hook should correctly manage the dependencies and execute the callback only when necessary.
- The hook should return the same as
useEffect(i.e., nothing).
Expected Behavior:
- On initial mount, the callback function should be executed.
- If the dependencies remain unchanged after the initial mount (based on deep comparison), the callback function should not be executed on subsequent renders.
- If the dependencies change (based on deep comparison), the callback function should be executed.
Edge Cases to Consider:
- Empty dependency array: The effect should run only on mount and never again.
- Dependencies that are primitive values (numbers, strings, booleans): Deep comparison should still work correctly.
- Dependencies that are objects or arrays: Deep comparison using
JSON.stringifyshould accurately detect changes. - Dependencies that are
nullorundefined: Handle these gracefully.
Examples
Example 1:
Input:
const [data, setData] = useState({ a: 1, b: 2 });
useDeepCompareEffect(() => {
console.log("Effect running");
}, [data]);
const handleClick = () => {
setData({ a: 1, b: 2 }); // No change
};
const handleClick2 = () => {
setData({ a: 1, b: 3 }); // Change
};
Output: "Effect running" (on initial mount), then nothing when handleClick is called. "Effect running" when handleClick2 is called.
Explanation: The effect runs on initial mount. handleClick doesn't change the deep stringified representation of data, so the effect doesn't re-run. handleClick2 does change the deep stringified representation, so the effect re-runs.
Example 2:
Input:
const [count, setCount] = useState(0);
useDeepCompareEffect(() => {
console.log("Effect running with count:", count);
}, [count]);
const increment = () => {
setCount(count + 1);
};
Output: "Effect running with count: 0" (on initial mount), then "Effect running with count: 1", "Effect running with count: 2", and so on, each time increment is called.
Explanation: count is a primitive value, so deep comparison using JSON.stringify will correctly detect changes.
Example 3:
Input:
const [items, setItems] = useState([]);
useDeepCompareEffect(() => {
console.log("Effect running");
}, []);
const addItem = () => {
setItems([...items, 1]);
};
Output: "Effect running" (on initial mount), then nothing when addItem is called.
Explanation: The dependency array is empty, so the effect runs only on initial mount and never again.
Constraints
- The hook must be implemented using functional components and hooks.
- Deep comparison must be performed using
JSON.stringify. - The hook must be compatible with React 18 or later.
- The hook should not introduce any unnecessary performance overhead. While
JSON.stringifyisn't the most performant, avoid adding other significant overhead.
Notes
- Consider using
useRefto store the previous values of the dependencies. - Remember to handle the initial mount scenario correctly.
- The deep comparison using
JSON.stringifyhas limitations (e.g., it doesn't handle circular references). For this exercise, this is acceptable. - Focus on the core functionality of deep comparison and effect execution. Error handling and advanced features are not required.
- Think about how to efficiently compare the current dependencies with the previous dependencies.