Hone logo
Hone
Problems

Create useEffectOnce Hook in React

As React developers, we often need to perform side effects that should only run once when a component mounts, similar to componentDidMount in class components. While useEffect with an empty dependency array ([]) achieves this, it's easy to accidentally omit the dependency array or introduce dependencies that cause the effect to re-run. This challenge asks you to create a custom React hook, useEffectOnce, that guarantees a side effect runs only a single time.

Problem Description

Your task is to implement a custom React hook named useEffectOnce in TypeScript. This hook should accept a callback function (the effect) as its primary argument and optionally a dependency array. The key requirement is that the provided effect function should execute exactly once during the component's lifecycle, regardless of the component's re-renders or changes in its props or state, unless explicitly overridden by a dependency array.

Key Requirements:

  • The hook must be named useEffectOnce.
  • It should accept a callback function of type React.EffectCallback.
  • It should optionally accept a dependency array of type DependencyArray (similar to useEffect).
  • The callback function should be executed only on the initial render of the component.
  • If a dependency array is provided, the hook should behave like useEffect with that dependency array, meaning the effect will re-run if any of the dependencies change. If no dependency array is provided, it should behave like useEffect(() => {...}, []).

Expected Behavior:

  • When a component using useEffectOnce mounts, the provided effect callback should run.
  • On subsequent re-renders of the same component, the effect callback should not run again if no dependencies were provided or if the provided dependencies have not changed.
  • If dependencies are provided and they change, the effect should re-run as expected by useEffect.

Edge Cases to Consider:

  • What happens if no dependency array is provided? (It should default to running only once).
  • What happens if an empty dependency array [] is provided? (It should run only once).
  • How does your hook handle cleanup functions returned by the effect callback?

Examples

Example 1: Running an effect once without dependencies.

import React, { useState } from 'react';
import { useEffectOnce } from './use-effect-once'; // Assuming your hook is in this file

function MyComponent() {
  const [message, setMessage] = useState('Loading...');

  useEffectOnce(() => {
    console.log('This effect runs only once on mount!');
    const timerId = setTimeout(() => {
      setMessage('Data loaded!');
    }, 2000);

    // Cleanup function
    return () => {
      console.log('Cleanup running on unmount.');
      clearTimeout(timerId);
    };
  }); // No dependency array provided

  return (
    <div>
      <h1>{message}</h1>
    </div>
  );
}

// Scenario:
// 1. MyComponent mounts.
// 2. The console logs "This effect runs only once on mount!".
// 3. The message state updates after 2 seconds to "Data loaded!".
// 4. MyComponent re-renders. The effect does NOT run again.
// 5. MyComponent unmounts. The console logs "Cleanup running on unmount.".

Example 2: Running an effect once initially, but re-running based on dependencies.

import React, { useState } from 'react';
import { useEffectOnce } from './use-effect-once';

function CounterComponent({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  const [logMessage, setLogMessage] = useState('');

  // This effect will run once initially, and then again if initialCount changes (though in this example, it's unlikely to change after initial mount of CounterComponent itself)
  useEffectOnce(() => {
    console.log(`Effect ran with initialCount: ${initialCount}. Current count: ${count}`);
    setLogMessage(`Effect ran with initialCount: ${initialCount}`);
  }, [initialCount]); // Dependency array includes initialCount

  const increment = () => setCount(prevCount => prevCount + 1);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Log: {logMessage}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// Scenario:
// 1. CounterComponent mounts with initialCount = 5.
// 2. The console logs "Effect ran with initialCount: 5. Current count: 5".
// 3. The user clicks "Increment" several times. The effect does NOT re-run.
// 4. If CounterComponent were re-rendered from its parent with a new initialCount prop, the effect *would* re-run.

Constraints

  • The hook must be implemented using TypeScript.
  • The hook should leverage standard React hooks like useEffect.
  • Avoid implementing custom hooks that bypass React's core mechanisms for managing effects.
  • The hook should be performant and not introduce unnecessary overhead.

Notes

  • Consider how the built-in useEffect hook works, especially regarding its dependency array and cleanup functions.
  • Your useEffectOnce hook should, in essence, be a more opinionated wrapper around useEffect.
  • Think about the types you need to use for the effect callback and the dependency array. React.EffectCallback and DependencyArray are good starting points.
Loading editor...
typescript