Hone logo
Hone
Problems

React useUnmount Hook Implementation

Your task is to create a custom React hook called useUnmount. This hook should provide a mechanism to execute a callback function precisely when a React component unmounts. This is crucial for tasks like cleaning up subscriptions, canceling timers, or performing any other necessary cleanup operations to prevent memory leaks and ensure application stability.

Problem Description

You need to implement a custom hook, useUnmount, that accepts a callback function as an argument. This callback function should be executed exactly once, at the moment the component where the hook is used unmounts.

Key Requirements:

  • The hook must accept a single argument: callback: () => void.
  • The callback function should be invoked only when the component using the hook is removed from the React tree.
  • The hook should be robust and handle scenarios where the callback might change between renders.
  • The hook should not introduce any performance overhead beyond what's necessary for its functionality.

Expected Behavior:

When a component utilizing useUnmount unmounts, the provided callback function should be executed. For example, if you have a component that subscribes to an event on mount, you would use useUnmount to unsubscribe from that event on unmount.

Edge Cases:

  • Callback changes: The hook should correctly use the latest version of the callback function if it changes between renders.
  • No callback provided: While not explicitly required, consider how your hook would behave (or document its expected behavior) if no callback is passed. (For this challenge, assume a callback will always be provided).

Examples

Example 1: Basic Usage

import React, { useState } from 'react';
import { useUnmount } from './useUnmount'; // Assuming you save your hook in this file

function Counter() {
  const [count, setCount] = useState(0);
  const [unmountMessage, setUnmountMessage] = useState<string | null>(null);

  useUnmount(() => {
    setUnmountMessage('Counter component is unmounting!');
    console.log('Counter component unmounted. Cleanup performed.');
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {unmountMessage && <p>{unmountMessage}</p>}
    </div>
  );
}

function App() {
  const [showCounter, setShowCounter] = useState(true);

  return (
    <div>
      {showCounter && <Counter />}
      <button onClick={() => setShowCounter(false)}>Unmount Counter</button>
    </div>
  );
}

Input: User clicks "Unmount Counter" button in the App component.

Output: The Counter component will be removed from the DOM. The console.log inside the useUnmount callback will execute, and the "Counter component is unmounting!" message will be displayed.

Explanation: When setShowCounter(false) is called, the Counter component is unmounted. The useUnmount hook within Counter intercepts this and executes its cleanup callback.

Example 2: Callback with Dependencies (Conceptual)

While useUnmount itself doesn't typically take dependencies like useEffect, the callback can capture values from its closure. The hook should ensure the latest callback is used.

import React, { useState, useEffect } from 'react';
import { useUnmount } from './useUnmount';

function TimerComponent() {
  const [seconds, setSeconds] = useState(0);

  // Let's simulate a scenario where the cleanup logic might depend on something
  // that could change, though for useUnmount, it's about the callback itself.
  const cleanupMessage = "Timer stopped.";

  const handleUnmount = () => {
    console.log(`Timer cleanup: ${cleanupMessage} - Final seconds: ${seconds}`);
    // In a real scenario, this could stop a timer, unsubscribe, etc.
  };

  useUnmount(handleUnmount); // The hook should capture the latest `handleUnmount`

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);

    // Cleanup for the useEffect itself, to prevent memory leaks
    return () => clearInterval(intervalId);
  }, []);

  return <div>Seconds elapsed: {seconds}</div>;
}

function App() {
  const [showTimer, setShowTimer] = useState(true);

  return (
    <div>
      {showTimer && <TimerComponent />}
      <button onClick={() => setShowTimer(false)}>Stop and Unmount Timer</button>
    </div>
  );
}

Input: User clicks "Stop and Unmount Timer" button after the TimerComponent has been rendered for some time.

Output: The TimerComponent unmounts. The console.log will output a message indicating the timer cleanup and the final seconds value captured by the callback when it was executed.

Explanation: Even if seconds were to change, the useUnmount hook ensures that the handleUnmount function, which captures the seconds value at the time of unmount, is executed.

Constraints

  • The hook must be implemented using TypeScript.
  • The hook should not rely on external libraries beyond standard React.
  • The implementation should be efficient and avoid unnecessary re-renders.

Notes

  • Think about how React handles component lifecycle and cleanup. The useEffect hook with a cleanup function is a strong hint.
  • Consider how to ensure the latest version of the callback is always used, even if the reference to the function changes between renders. This is a common pattern in custom React hooks.
  • Your implementation should be placed in a file (e.g., useUnmount.ts) and then imported into components that need it.
Loading editor...
typescript