DEV Community

ZeeshanAli-0704
ZeeshanAli-0704

Posted on

Polyfill - useEffect (React)

Below, I'll provide the code with detailed comments, an explanation of each part, and the expected output. I'll build on the index-tracking mechanism from the useState polyfill to ensure consistency across renders for useEffect calls.

Overview of useEffect

  • Purpose: useEffect is a React hook that lets you perform side effects in functional components. It runs after every render by default (or conditionally based on dependencies) and can handle cleanup (e.g., unsubscribing from events).
  • Polyfill Goal: Simulate the behavior of running an effect after a render and optionally running a cleanup function when the component "unmounts" or dependencies change.

Simple useEffect Polyfill Code with Detailed Comments

// Simple useEffect polyfill for interview explanation
function createUseEffect() {
  // Array to store effect functions and their dependencies across "renders"
  let effectStore = [];
  // Track current index for hook calls in a render
  let currentIndex = 0;
  // Track if this is the first render for each effect
  let isFirstRender = [];

  // The useEffect function, mimicking React's hook for side effects
  function useEffect(effectFn, dependencies) {
    // Capture the current index for this specific useEffect call
    const index = currentIndex;
    currentIndex++;

    // Initialize storage for this effect if not set yet
    if (effectStore[index] === undefined) {
      effectStore[index] = { fn: effectFn, deps: dependencies, prevDeps: null };
      isFirstRender[index] = true;
    }

    // Check if dependencies have changed (or it's the first render)
    // In real React, useEffect runs on mount and when deps change
    const shouldRun = isFirstRender[index] || !areDepsEqual(effectStore[index].prevDeps, dependencies);

    if (shouldRun) {
      // Run the effect function if it's the first render or deps changed
      console.log(`Running effect at index ${index}`);
      const cleanup = effectStore[index].fn();
      // Store any cleanup function returned by the effect (if any)
      effectStore[index].cleanup = cleanup;
      // Update previous dependencies for next comparison
      effectStore[index].prevDeps = dependencies;
      // Mark as no longer first render
      isFirstRender[index] = false;
    }

    // Return nothing (useEffect doesn't return a value like useState)
  }

  // Helper function to check if dependencies are equal
  // In real React, this is a shallow comparison
  function areDepsEqual(oldDeps, newDeps) {
    if (oldDeps === null || newDeps === null) return false;
    if (oldDeps.length !== newDeps.length) return false;
    for (let i = 0; i < oldDeps.length; i++) {
      if (oldDeps[i] !== newDeps[i]) return false;
    }
    return true;
  }

  // Reset index for next render simulation
  function resetIndex() {
    currentIndex = 0;
    console.log('Resetting index for next render');
  }

  // Simulate component unmount by running cleanup functions
  function unmount() {
    console.log('Simulating component unmount - running cleanups');
    effectStore.forEach((effect, index) => {
      if (effect.cleanup && typeof effect.cleanup === 'function') {
        console.log(`Running cleanup for effect at index ${index}`);
        effect.cleanup();
      }
    });
    // Clear effect store on unmount (optional, for simulation)
    effectStore = [];
    isFirstRender = [];
  }

  return { useEffect, resetIndex, unmount };
}

// Create an instance of useEffect
const { useEffect, resetIndex, unmount } = createUseEffect();

// Simulated component to show useEffect usage
function MyComponent(count, name) {
  // Simulate useState for context (though not fully implemented here)
  console.log('Render - Count:', count, 'Name:', name);

  // Use useEffect to log a message on mount and when count changes
  useEffect(() => {
    console.log(`Effect: Count changed to ${count}`);
    // Return a cleanup function (simulates unsubscribing or cleanup)
    return () => {
      console.log(`Cleanup: Count effect for ${count}`);
    };
  }, [count]); // Dependency array: run effect when count changes

  // Use useEffect to log a message only on mount (empty deps)
  useEffect(() => {
    console.log('Effect: Component mounted');
    // Return a cleanup function for unmount
    return () => {
      console.log('Cleanup: Component unmounted');
    };
  }, []); // Empty dependency array: run only on mount

  // No setters or return needed for this simulation
}

// Run the simulation to mimic React rendering
console.log('First Call (Initial Render):');
MyComponent(0, "Zeeshan");
resetIndex();

console.log('\nSecond Call (After Count Update):');
MyComponent(1, "Zeeshan");
resetIndex();

console.log('\nThird Call (After Name Update):');
MyComponent(1, "John");
resetIndex();

console.log('\nSimulating Unmount:');
unmount();
Enter fullscreen mode Exit fullscreen mode

How to Execute

  1. In a Browser: Open your browser's developer tools (e.g., Chrome DevTools), go to the "Console" tab, copy-paste the code above, and press Enter. You'll see logs showing renders, effects running, and cleanups on unmount.
  2. In Node.js: Save this code in a file (e.g., useEffectPolyfill.js) and run it using node useEffectPolyfill.js in your terminal. The output will appear in the console.

Expected Output

First Call (Initial Render):
Render - Count: 0 Name: Zeeshan
Running effect at index 0
Effect: Count changed to 0
Running effect at index 1
Effect: Component mounted
Resetting index for next render

Second Call (After Count Update):
Render - Count: 1 Name: Zeeshan
Running effect at index 0
Effect: Count changed to 1
Resetting index for next render

Third Call (After Name Update):
Render - Count: 1 Name: John
Resetting index for next render

Simulating Unmount:
Simulating component unmount - running cleanups
Running cleanup for effect at index 0
Cleanup: Count effect for 1
Running cleanup for effect at index 1
Cleanup: Component unmounted
Enter fullscreen mode Exit fullscreen mode

Detailed Explanation of Each Section

  1. State Storage and Index Tracking (effectStore, currentIndex, isFirstRender):

    • effectStore: Stores the effect function, dependencies, previous dependencies, and any cleanup function for each useEffect call.
    • currentIndex: Tracks the index for each useEffect call in a render, ensuring consistent mapping (e.g., first useEffect is always index 0).
    • isFirstRender: Tracks if it's the first render for each effect to run it on "mount."
    • Why: Mimics React’s internal tracking of effects per component, maintaining order.
  2. useEffect Function:

    • What: Assigns an index, checks if the effect should run (first render or deps changed), runs the effect function if needed, and stores any cleanup returned.
    • How: Compares dependencies using areDepsEqual (shallow comparison) to decide if the effect runs. Updates prevDeps for next render comparison.
    • Example: First render runs both effects (index 0 and 1). Second render runs only the effect with [count] as deps when count changes from 0 to 1.
  3. areDepsEqual Helper:

    • What: Performs a shallow comparison of dependency arrays to check if they’ve changed.
    • Why: In React, useEffect reruns only if dependencies change. This simulates that logic.
    • Example: [0] vs [1] returns false (runs effect); [1] vs [1] returns true (skips effect).
  4. resetIndex Function:

    • What: Resets currentIndex to 0 after a render to prepare for the next simulation.
    • Why: Simulates React resetting hook indices per render to maintain call order.
    • Example: After first render, currentIndex is 2, then reset to 0 for next render.
  5. unmount Function:

    • What: Runs cleanup functions for all effects, simulating component unmount.
    • Why: In React, cleanup runs on unmount or before a new effect if deps change. This shows cleanup behavior.
    • Example: Logs cleanup messages for both effects when unmount() is called.
  6. MyComponent Function and Simulation:

    • What: Simulates a component using useEffect for side effects (logging based on count and mounting).
    • How: Passes different count and name values across renders to show dependency behavior.
    • Example: First render runs both effects. Second render runs only count-dependent effect. Third render runs no effects (no deps changed). Unmount runs cleanups.

Key Interview Talking Points

  • Purpose of useEffect Polyfill: Explain that it simulates React’s useEffect for handling side effects (e.g., data fetching, subscriptions) in functional components, running after renders based on dependencies.
  • Effect Execution: Highlight that effects run on first render ("mount") and when dependencies change, mimicking React’s behavior.
  • Dependency Array: Stress the role of the dependency array—empty [] runs only on mount, specific values like [count] run when those values change.
  • Cleanup: Mention cleanup functions (returned by effects) run on unmount or before a new effect if deps change, shown via unmount() simulation.
  • Limitations: Note this is a basic version. Real React integrates with the DOM, batches effects, handles async effects, and runs cleanup before new effects, which this manual simulation simplifies.

This useEffect polyfill follows the same index-tracking approach as the useState polyfill, keeping it consistent and easy to explain. If you’d like to combine it with useState, add more effects, or explore specific scenarios, let me know!

Top comments (0)