Hone logo
Hone
Problems

Selective Hydration for React Components

Modern web applications often fetch data asynchronously to populate their UIs. To improve perceived performance, particularly for server-rendered applications, we want to implement selective hydration. This means only rendering and attaching event listeners to the parts of the page that are immediately visible or interactable by the user, deferring the hydration of other components until they are needed.

Problem Description

Your task is to implement a mechanism for selective hydration in a React application using TypeScript. You'll create a SelectiveHydrationProvider and a useSelectiveHydration hook that allow components to opt-in to being hydrated only when they are within the viewport or when explicitly triggered.

Key Requirements:

  1. SelectiveHydrationProvider: This component will wrap your application or a specific section of it. It should manage the state of which components are ready for hydration.
  2. useSelectiveHydration Hook: Components will use this hook to register themselves for selective hydration. The hook should return a value indicating whether the component is hydrated and ready for full interactivity.
  3. Viewport Detection: The provider should automatically trigger hydration for components that come into the viewport.
  4. Manual Trigger: Components should have a way to manually trigger their own hydration, even if they are not in the viewport.
  5. State Management: The provider needs to efficiently manage which components are hydrated and re-render only the necessary components when hydration status changes.
  6. Server-Side Rendering (SSR) Compatibility: The solution should be mindful of SSR, where components are initially rendered on the server without interactivity. Hydration on the client re-attaches this interactivity.

Expected Behavior:

  • When the application loads, only components not using selective hydration, or those explicitly marked as "eager," should be fully interactive.
  • Components using useSelectiveHydration should render their static UI initially (as if they were server-rendered).
  • As components enter the viewport, the SelectiveHydrationProvider should detect this and signal them to hydrate.
  • When a component receives the signal to hydrate, it should become fully interactive (e.g., event listeners are attached).
  • A component using useSelectiveHydration should expose a hydrateComponent function that, when called, forces its hydration regardless of viewport status.

Edge Cases:

  • Components that are already hydrated before entering the viewport should not be re-hydrated.
  • The provider should handle a large number of selectively hydrated components without performance degradation.
  • Ensure proper cleanup of observers and subscriptions when components unmount.

Examples

Example 1: Basic Viewport Hydration

Scenario: A page with several components, some eager and some selective. A selective component is initially below the fold.

Component Structure (Conceptual):

// App.tsx
import SelectiveHydrationProvider from './SelectiveHydrationProvider';
import EagerComponent from './EagerComponent';
import SelectiveComponent from './SelectiveComponent';
import './styles.css'; // Assume this has CSS to push SelectiveComponent below fold

function App() {
  return (
    <SelectiveHydrationProvider>
      <h1>My App</h1>
      <EagerComponent />
      <div style={{ height: '150vh' }}></div> {/* Spacer to push SelectiveComponent down */}
      <SelectiveComponent initialMessage="Hello from below the fold!" />
    </SelectiveHydrationProvider>
  );
}

// EagerComponent.tsx
import React from 'react';

function EagerComponent() {
  const handleClick = () => alert('Eager component clicked!');
  return <button onClick={handleClick}>Eager Button</button>;
}

// SelectiveComponent.tsx
import React, { useState } from 'react';
import { useSelectiveHydration } from './SelectiveHydrationProvider'; // Assuming hook is exported from provider file

interface SelectiveComponentProps {
  initialMessage: string;
}

function SelectiveComponent({ initialMessage }: SelectiveComponentProps) {
  const { isHydrated, hydrateComponent } = useSelectiveHydration();
  const [message, setMessage] = useState(initialMessage);

  const handleClick = () => {
    if (isHydrated) {
      alert(`Selective component clicked! Message: ${message}`);
    } else {
      console.log('Selective component clicked, but not yet hydrated.');
      // Optionally, prompt user to hydrate or show a loading state
    }
  };

  const handleHydrateButtonClick = () => {
    hydrateComponent();
    console.log('Manual hydrate triggered.');
  };

  return (
    <div>
      <p>{message}</p>
      <button onClick={handleClick} disabled={!isHydrated}>
        {isHydrated ? 'Interact with Selective Component' : 'Waiting for hydration...'}
      </button>
      {!isHydrated && (
        <button onClick={handleHydrateButtonClick}>Force Hydrate Now</button>
      )}
    </div>
  );
}

Initial Render (SSR/No Hydration):

  • EagerComponent is rendered with an interactive button.
  • SelectiveComponent is rendered with its static content (<p>Hello from below the fold!</p>) and its buttons are in a non-interactive state (or display "Waiting..."). The "Interact" button is disabled.

After Scrolling:

  • When SelectiveComponent scrolls into the viewport, the SelectiveHydrationProvider detects this.
  • The provider signals SelectiveComponent to hydrate.
  • SelectiveComponent becomes fully hydrated. Its "Interact" button becomes enabled and functional. The "Force Hydrate Now" button might disappear or change state.

Example 2: Manual Hydration Trigger

Scenario: A component that is initially off-screen but the user explicitly wants to interact with it before it scrolls into view.

Component Structure (Similar to Example 1):

Modify SelectiveComponent to include a scenario where hydrateComponent is called early.

// SelectiveComponent.tsx (modified)
import React, { useState } from 'react';
import { useSelectiveHydration } from './SelectiveHydrationProvider';

interface SelectiveComponentProps {
  initialMessage: string;
}

function SelectiveComponent({ initialMessage }: SelectiveComponentProps) {
  const { isHydrated, hydrateComponent } = useSelectiveHydration();
  const [message, setMessage] = useState(initialMessage);
  const [manuallyHydrated, setManuallyHydrated] = useState(false); // Track if manual hydrate was called

  const handleClick = () => {
    if (isHydrated) {
      alert(`Selective component clicked! Message: ${message}`);
    } else {
      console.log('Selective component clicked, but not yet hydrated.');
    }
  };

  const handleHydrateButtonClick = () => {
    hydrateComponent();
    setManuallyHydrated(true); // Mark that we triggered hydration
    console.log('Manual hydrate triggered.');
  };

  return (
    <div>
      <p>{message}</p>
      <button onClick={handleClick} disabled={!isHydrated}>
        {isHydrated ? 'Interact with Selective Component' : 'Waiting for hydration...'}
      </button>
      {/* Show button if not hydrated OR if it was manually hydrated but still not fully interactive */}
      {!isHydrated || !manuallyHydrated ? (
        <button onClick={handleHydrateButtonClick}>Force Hydrate Now</button>
      ) : null}
    </div>
  );
}

Behavior:

  • The user clicks the "Force Hydrate Now" button on SelectiveComponent even before it's in the viewport.
  • hydrateComponent() is called.
  • SelectiveComponent becomes fully hydrated immediately, regardless of its scroll position.
  • The "Interact" button becomes enabled.

Constraints

  • The SelectiveHydrationProvider and useSelectiveHydration hook should be implemented using React Hooks and TypeScript.
  • Intersection Observer API (or a similar mechanism) should be used for viewport detection.
  • The solution should not introduce significant overhead for components that are not using selective hydration.
  • The hydration process for a single component should ideally not block the main thread for an extended period. Consider strategies for breaking up large hydration tasks if necessary (though for this challenge, basic hydration is sufficient).
  • The solution should be compatible with React 18's concurrent features (though explicit use of startTransition isn't strictly required for the core challenge, awareness is good).

Notes

  • Think about how the SelectiveHydrationProvider will communicate with the child components that need hydration. A context API is a common and effective pattern for this.
  • Consider what information each selectively hydrating component needs to pass to the provider (e.g., a unique identifier, a callback to signal hydration completion).
  • The state of hydration should be granular to each component.
  • You'll need to create a mechanism to register components with the provider.
  • When a component is hydrated, it should essentially "unmount" its SSR'd version and "mount" its client-side interactive version. In React, this often means re-rendering the component with its event handlers and state management.
Loading editor...
typescript