Hone logo
Hone
Problems

Implement a Custom useInsertionEffect Hook in React

React's useInsertionEffect hook is designed for CSS-in-JS libraries to inject styles into the DOM before the browser performs layout and paints. This challenge asks you to implement a custom hook that mimics this behavior, allowing you to understand its lifecycle and purpose.

Problem Description

Your task is to create a custom hook named useCustomInsertionEffect that functions similarly to React's useInsertionEffect. This hook should accept a callback function and an optional dependency array. The callback function should be executed synchronously after the DOM mutations are committed but before the browser's paint, specifically for scenarios like injecting styles.

Key Requirements:

  • The hook must accept a callback function (setup) as its first argument.
  • The hook must accept an optional dependency array (dependencies) as its second argument.
  • The setup function should be executed synchronously when the component mounts and whenever any of the dependencies change.
  • The setup function can optionally return a cleanup function. This cleanup function should be executed synchronously before the next effect runs or when the component unmounts.
  • The hook should be implemented in TypeScript.

Expected Behavior:

When a component uses useCustomInsertionEffect, the provided setup function should run at the correct point in the React rendering lifecycle. This means it should run after the DOM has been updated but before the browser paints the screen. This is crucial for performance-sensitive operations like style injection that need to be reflected immediately.

Edge Cases:

  • No Dependency Array: If no dependency array is provided, the effect should run after every render.
  • Empty Dependency Array: If an empty dependency array is provided, the effect should run only once after the initial render.
  • Cleanup Function: Ensure the cleanup function is correctly called before the effect re-runs or on unmount.

Examples

Example 1: Simple Style Injection

import React, { useState } from 'react';
import { useCustomInsertionEffect } from './useCustomInsertionEffect'; // Assuming your hook is in this file

function MyStyledComponent({ color }: { color: string }) {
  const [styleId, setStyleId] = useState<string | null>(null);

  useCustomInsertionEffect(() => {
    const id = `my-dynamic-style-${Math.random().toString(36).substr(2, 9)}`;
    const styleElement = document.createElement('style');
    styleElement.id = id;
    styleElement.innerHTML = `
      .dynamic-text {
        color: ${color};
      }
    `;
    document.head.appendChild(styleElement);
    setStyleId(id);

    // Cleanup function
    return () => {
      const elementToRemove = document.getElementById(id);
      if (elementToRemove) {
        elementToRemove.remove();
      }
    };
  }, [color]); // Re-run effect if color changes

  return (
    <div className="dynamic-text">
      This text color is dynamically set. Current color: {color}
    </div>
  );
}

function App() {
  const [show, setShow] = useState(true);
  const [currentColor, setCurrentColor] = useState('blue');

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        {show ? 'Hide' : 'Show'} Component
      </button>
      <button onClick={() => setCurrentColor(currentColor === 'blue' ? 'red' : 'blue')}>
        Toggle Color
      </button>
      {show && <MyStyledComponent color={currentColor} />}
    </div>
  );
}

Input to the example: The App component renders MyStyledComponent and provides a color prop. Buttons allow toggling visibility of MyStyledComponent and changing its color prop.

Expected Output:

  • When MyStyledComponent mounts, a <style> tag is injected into the <head> with the class .dynamic-text styled according to the initial color prop.
  • When the "Toggle Color" button is clicked, the color prop changes. useCustomInsertionEffect detects this dependency change, removes the old style, and injects a new one with the updated color.
  • When the "Hide Component" button is clicked, MyStyledComponent unmounts. The cleanup function within useCustomInsertionEffect runs, removing the injected <style> tag from the <head>.
  • The text inside MyStyledComponent will visually change color accordingly.

Example 2: No Dependency Array

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

function CounterWithEffect() {
  const [count, setCount] = useState(0);

  useCustomInsertionEffect(() => {
    console.log('Effect running (no dependencies)');
    const styleElement = document.createElement('style');
    styleElement.innerHTML = `body { background-color: lightgray; }`;
    document.head.appendChild(styleElement);

    return () => {
      console.log('Cleanup running (no dependencies)');
      styleElement.remove();
    };
  }); // No dependency array

  console.log('Rendering CounterWithEffect');

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

Input to the example: The CounterWithEffect component renders a count and an increment button. It uses useCustomInsertionEffect without a dependency array.

Expected Output:

  • On initial render, "Rendering CounterWithEffect" is logged, then "Effect running (no dependencies)" is logged, and a style is injected.
  • When the "Increment" button is clicked, "Rendering CounterWithEffect" is logged, then the previous style is cleaned up ("Cleanup running (no dependencies)"), and a new style is injected ("Effect running (no dependencies)"). This will happen on every render.

Constraints

  • The hook should be implemented in TypeScript.
  • The hook must adhere to the described lifecycle of useInsertionEffect.
  • The implementation should be as efficient as possible, avoiding unnecessary re-renders or DOM operations.
  • The hook should not rely on any external libraries beyond React.

Notes

  • Think about how React manages effects and dependencies internally. You'll likely need to leverage React's existing hooks to achieve the correct timing and dependency management.
  • Consider the order of operations: mount -> effect -> paint -> update -> cleanup -> effect -> paint.
  • useInsertionEffect is called synchronously after all DOM mutations but before the browser has a chance to paint. This is different from useEffect which runs asynchronously after paint.
  • You'll need to track the previous dependencies to determine if the effect should re-run.
Loading editor...
typescript