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:
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.useSelectiveHydrationHook: 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.- Viewport Detection: The provider should automatically trigger hydration for components that come into the viewport.
- Manual Trigger: Components should have a way to manually trigger their own hydration, even if they are not in the viewport.
- State Management: The provider needs to efficiently manage which components are hydrated and re-render only the necessary components when hydration status changes.
- 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
useSelectiveHydrationshould render their static UI initially (as if they were server-rendered). - As components enter the viewport, the
SelectiveHydrationProvidershould 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
useSelectiveHydrationshould expose ahydrateComponentfunction 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):
EagerComponentis rendered with an interactive button.SelectiveComponentis 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
SelectiveComponentscrolls into the viewport, theSelectiveHydrationProviderdetects this. - The provider signals
SelectiveComponentto hydrate. SelectiveComponentbecomes 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
SelectiveComponenteven before it's in the viewport. hydrateComponent()is called.SelectiveComponentbecomes fully hydrated immediately, regardless of its scroll position.- The "Interact" button becomes enabled.
Constraints
- The
SelectiveHydrationProvideranduseSelectiveHydrationhook 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
startTransitionisn't strictly required for the core challenge, awareness is good).
Notes
- Think about how the
SelectiveHydrationProviderwill 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.