Hone logo
Hone
Problems

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 useDeepCompareEffect that 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.stringify for 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.stringify should accurately detect changes.
  • Dependencies that are null or undefined: 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.stringify isn't the most performant, avoid adding other significant overhead.

Notes

  • Consider using useRef to store the previous values of the dependencies.
  • Remember to handle the initial mount scenario correctly.
  • The deep comparison using JSON.stringify has 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.
Loading editor...
typescript