Hone logo
Hone
Problems

React Transition Tracker

This challenge focuses on building a reusable React component that can track and display the transition state of an element. This is crucial for implementing smooth animations and visual feedback when elements enter, exit, or change within a React application.

Problem Description

Your task is to create a React hook, useTransitionTracker, that simplifies the process of managing and displaying the transition state of a React element. This hook should provide a way to determine if an element is currently entering, exiting, or has settled.

Key Requirements:

  1. State Management: The hook must manage three distinct states: entering, exiting, and idle.
  2. Duration Control: The hook should accept a duration prop to define how long the entering and exiting states persist.
  3. Conditional Rendering/Styling: The hook should return a set of values that allow the parent component to conditionally render or apply styles based on the current transition state.
  4. Entry Trigger: The hook needs a mechanism to signal that an element is about to enter or has just entered.
  5. Exit Trigger: The hook needs a mechanism to signal that an element is about to exit or has just exited.

Expected Behavior:

  • When the hook is initialized or a new item is added, it should enter the entering state for the specified duration.
  • When an item is marked for removal, it should enter the exiting state for the specified duration.
  • After the duration has passed for entering or exiting, the state should transition to idle.
  • The hook should be able to handle multiple items being tracked concurrently.

Edge Cases to Consider:

  • Rapid entry and exit requests for the same item.
  • Setting a duration of 0.
  • The initial state when no items are being tracked.

Examples

Example 1: Basic Entry

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

function App() {
  const [showDiv, setShowDiv] = useState(false);
  const { isEntering, isExiting, isIdle } = useTransitionTracker({
    duration: 500, // 500ms
    shouldTrack: showDiv, // Track when showDiv is true
  });

  return (
    <div>
      <button onClick={() => setShowDiv(true)}>Show Div</button>
      <button onClick={() => setShowDiv(false)} disabled={!showDiv}>Hide Div</button>

      {showDiv && ( // Render the div only when showDiv is true
        <div
          style={{
            opacity: isEntering ? 0.5 : isExiting ? 0.5 : 1,
            transition: 'opacity 0.5s ease-in-out',
          }}
        >
          This div is transitioning!
        </div>
      )}
    </div>
  );
}

// Assume useTransitionTracker hook is implemented to return these states
// When showDiv becomes true, isEntering should be true for 500ms, then idle.
// When showDiv becomes false, isExiting should be true for 500ms, then idle.

Output:

Initially, nothing is shown. When "Show Div" is clicked, the div appears. For the first 500ms, isEntering will be true, and the div will have reduced opacity (as per the style). After 500ms, isEntering becomes false, and isIdle becomes true. When "Hide Div" is clicked, the div remains visible but isExiting becomes true for 500ms, with reduced opacity. After 500ms, the div is removed from the DOM because showDiv is false.

Example 2: Multiple Items

// Parent Component
import React, { useState } from 'react';
import { useTransitionTracker } from './useTransitionTracker';

interface Item {
  id: number;
  text: string;
}

function App() {
  const [items, setItems] = useState<Item[]>([]);
  const [nextId, setNextId] = useState(1);

  // This hook would need to handle an array of trackable items
  // For simplicity, let's imagine it manages a single trackable state
  // and the parent handles item-specific logic. A more advanced hook
  // might take an array of IDs to track.
  const { isEntering, isExiting, isIdle } = useTransitionTracker({
    duration: 300,
    // This is a simplified representation. A real hook would need to manage
    // tracking for each individual item.
    shouldTrack: items.length > 0,
  });

  const addItem = () => {
    setItems([...items, { id: nextId, text: `Item ${nextId}` }]);
    setNextId(nextId + 1);
  };

  const removeItem = (idToRemove: number) => {
    // This would trigger the "exiting" phase for the specific item
    // In a real implementation, you'd have a way to associate an ID with the trackable state.
    // For this example, we'll simulate removal after a delay.
    setTimeout(() => {
      setItems(items.filter(item => item.id !== idToRemove));
    }, 300); // Match the duration
  };

  return (
    <div>
      <button onClick={addItem}>Add Item</button>

      {items.map(item => (
        <div key={item.id} style={{ marginBottom: '10px' }}>
          {/*
            Here, you'd need to use the hook's results to style this individual item.
            A more robust hook might return an object of states keyed by item ID.
            For this example, we'll assume isEntering/isExiting reflect the *most recent* action.
          */}
          <span
            style={{
              display: 'inline-block',
              padding: '10px',
              border: '1px solid gray',
              opacity: isEntering ? 0.5 : isExiting ? 0.5 : 1,
              transition: 'opacity 0.3s ease-in-out',
            }}
          >
            {item.text}
          </span>
          <button onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

Output:

Initially, no items are present. Clicking "Add Item" adds an item with reduced opacity for 300ms (isEntering is true), then it becomes fully opaque (isIdle is true). If multiple items are added rapidly, they will all briefly have reduced opacity. Clicking "Remove" on an item will trigger a transition where isExiting is true for 300ms (reduced opacity), and then the item is removed from the DOM.

Example 3: Zero Duration

// Parent Component
import React, { useState } from 'react';
import { useTransitionTracker } from './useTransitionTracker';

function App() {
  const [isVisible, setIsVisible] = useState(false);
  const { isEntering, isExiting, isIdle } = useTransitionTracker({
    duration: 0, // Immediate transition
    shouldTrack: isVisible,
  });

  return (
    <div>
      <button onClick={() => setIsVisible(true)}>Show</button>
      <button onClick={() => setIsVisible(false)} disabled={!isVisible}>Hide</button>

      {isVisible && (
        <div
          style={{
            opacity: isEntering ? 0.5 : isExiting ? 0.5 : 1,
            transition: 'opacity 0s', // Instantaneous transition
          }}
        >
          Instant Transition
        </div>
      )}
    </div>
  );
}

Output:

When "Show" is clicked, shouldTrack becomes true. If duration is 0, the isEntering state should ideally be very brief or bypassed, transitioning to isIdle immediately. The div appears fully opaque. When "Hide" is clicked, shouldTrack becomes false. Similarly, with duration: 0, isExiting should be very brief or bypassed, and the div is removed from the DOM without any visual transition.

Constraints

  • The duration prop will be a non-negative integer representing milliseconds.
  • The shouldTrack prop will be a boolean.
  • The hook should ideally be performant enough to handle tracking dozens of concurrent transitions without noticeable lag.
  • The hook must be written in TypeScript.

Notes

  • Consider how you will manage the timer for the duration. setTimeout is likely your friend here.
  • You'll need to think about how to reset the state correctly when shouldTrack changes from true to false and vice-versa.
  • For the useTransitionTracker hook, the shouldTrack prop signifies when the element should be present. The hook's internal logic will determine if it's entering, exiting, or idle based on changes to this prop and the duration.
  • A more advanced version of this hook might involve passing an array of items to track, each with its own unique identifier. For this challenge, focus on a single shouldTrack boolean.
Loading editor...
typescript