React Effect Tracker: Monitor and Debug Component Effects
This challenge focuses on building a custom React hook that tracks the execution of other React hooks, specifically focusing on useEffect. This is incredibly useful for understanding the lifecycle of your components, debugging unintended re-renders or effect executions, and gaining deeper insights into how your React application behaves.
Problem Description
Your task is to create a custom React hook, let's call it useEffectTracker, that wraps the standard useEffect hook. This wrapper should provide a way to log or otherwise track when a useEffect hook runs, including its dependencies and the values of those dependencies at the time of execution.
Key Requirements:
- Hook Creation: Implement a custom hook
useEffectTrackerthat accepts the same arguments asReact.useEffect: a callback function and an optional dependency array. - Execution Logging: When the wrapped
useEffectis triggered (either on mount or when dependencies change), log a message to the console indicating that the effect has run. - Dependency Tracking: The log message should also include the current values of the dependencies that triggered the effect.
- Cleanup Tracking: If the
useEffectcallback returns a cleanup function, track and log when this cleanup function is executed. - Return Value: The
useEffectTrackerhook should behave identically toReact.useEffectin terms of returning a cleanup function if provided.
Expected Behavior:
When a component uses useEffectTracker, every time its useEffect logic executes, you should see informative logs in your browser's developer console. This will help you pinpoint exactly when and why effects are running.
Edge Cases to Consider:
- No Dependency Array: How does your tracker handle
useEffectcalls without a dependency array (runs on every render)? - Empty Dependency Array: How does your tracker handle
useEffectcalls with an empty dependency array (runs only on mount and unmount)? - Cleanup Function: Ensure cleanup logging works correctly.
- Dependencies of Various Types: Handle primitive types (strings, numbers, booleans), objects, and arrays as dependencies.
Examples
Let's imagine a simple Counter component.
Example 1: Basic Effect Tracking
-
Input Component Code:
import React, { useState } from 'react'; import { useEffectTracker } from './useEffectTracker'; // Assuming you've created this hook function Counter() { const [count, setCount] = useState(0); useEffectTracker(() => { console.log('Effect ran because count changed!'); return () => { console.log('Cleanup ran for count effect.'); }; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } -
Interaction: User clicks the "Increment" button once.
-
Expected Console Output:
Effect ran because count changed! Dependencies: { count: 1 } Cleanup ran for count effect.(Note: The exact dependency object might vary, but it should reflect the state at the time of execution. The cleanup will run on the next effect execution or unmount.)
Example 2: Tracking Effects with No Dependencies
-
Input Component Code:
import React, { useState } from 'react'; import { useEffectTracker } from './useEffectTracker'; function WelcomeMessage() { const [message, setMessage] = useState('Hello'); useEffectTracker(() => { console.log('This effect runs on every render.'); }); // No dependency array return ( <div> <p>{message}</p> <button onClick={() => setMessage('Greetings!')}>Change Message</button> </div> ); } -
Interaction: User clicks the "Change Message" button.
-
Expected Console Output:
This effect runs on every render. Dependencies: (no dependencies)(This log would appear again on subsequent renders as well.)
Example 3: Tracking Effects with Empty Dependencies
-
Input Component Code:
import React, { useState } from 'react'; import { useEffectTracker } from './useEffectTracker'; function MountOnlyComponent() { const [mounted, setMounted] = useState(true); useEffectTracker(() => { console.log('This effect runs only on mount.'); return () => { console.log('This cleanup runs only on unmount.'); }; }, []); // Empty dependency array return ( <div> <p>Component is mounted.</p> <button onClick={() => setMounted(false)}>Unmount</button> </div> ); } -
Interaction: Component mounts, then user clicks "Unmount".
-
Expected Console Output:
This effect runs only on mount. Dependencies: {} This cleanup runs only on unmount.
Constraints
- Your
useEffectTrackerhook must be implemented using TypeScript. - The logging should be done using
console.log. - Do not modify the behavior of the underlying
useEffecthook; it should still trigger re-renders or cleanup as intended by React. - Your solution should be a standalone custom hook.
Notes
- Consider how you will represent the dependencies in your log message. A simple object mapping dependency name (if possible to infer) to its value, or just an array of values, could work. For simplicity, logging the array of dependency values might be sufficient.
- You might want to use
useRefto store previous dependency values to detect changes accurately if you decide to log "why" an effect ran (e.g., "dependency X changed from Y to Z"). However, for this challenge, simply logging the current dependency values when the effect runs is the primary goal. - Think about the lifecycle of the
useEffectcallback and its cleanup function. When are they executed relative to each other? - The goal is to create a reusable tool for understanding effect behavior, not necessarily a full-blown performance profiler.