react-tree-stream
Version:
Stream React trees recursively, LLM-style progressive rendering.
213 lines (208 loc) • 8.11 kB
TypeScript
import * as react_jsx_runtime from 'react/jsx-runtime';
import React from 'react';
/**
* TreeStream
*
* A client-only React component that renders its children incrementally over time.
* It walks the provided React node tree into a linear execution plan of units:
* - text units (streamed by word or character)
* - instant units (regular React elements rendered immediately)
* - nested stream units (child TreeStream elements, coordinated by onComplete)
*
* Contract (inputs/outputs):
* - Props:
* - as: optional polymorphic element type; use 'fragment' for no wrapper
* - children: any renderable React nodes; fragments/arrays are flattened
* - speed: number of tokens per tick (tokens are words or characters)
* - interval: ms between ticks
* - streamBy: 'word' | 'character' determines tokenization of text units
* - autoStart: start streaming automatically when inputs/signature change
* - onComplete: called after the final unit completes (including nested)
* - DOM: adds data attributes for observability:
* - data-tree-stream, data-streaming, data-complete
* - SSR: client-only ('use client'); streaming occurs in the browser
*
* Notes:
* - Nested TreeStream children have autoStart forced to true, and their
* onComplete is composed so the parent resumes after the child completes.
* - A stable "plan signature" is used to reset the stream only when structure
* or text content changes; this limits unnecessary restarts.
*/
/**
* Props for TreeStream.
* - speed: tokens per tick (>= 1). Tokens are words or characters per streamBy.
* - interval: delay between ticks in ms.
* - streamBy: tokenization strategy for text nodes.
* - autoStart: if false, the component initializes idle until inputs change again or programmatically started in a future version.
*/
/**
* Core properties for the TreeStream component
*/
type OwnProps = {
/**
* Number of tokens to display per tick (must be >= 1).
* Tokens are either words or characters based on the `streamBy` prop.
* @default 5
*/
speed?: number;
/**
* Delay between ticks in milliseconds.
* Controls the animation speed of the streaming effect.
* @default 50
*/
interval?: number;
/**
* Tokenization strategy for text nodes.
* - 'word': Text is split and streamed word by word
* - 'character': Text is split and streamed character by character
* @default 'word'
*/
streamBy?: 'word' | 'character';
/**
* Whether to automatically start streaming when the component mounts
* or when inputs/signature change. If false, the component initializes
* in an idle state.
* @default true
*/
autoStart?: boolean;
/**
* Callback invoked after all content (including nested TreeStream components)
* has finished streaming.
*/
onComplete?: () => void;
};
type AsProp<E extends React.ElementType> = {
as?: E;
};
type PropsToOmit<E extends React.ElementType> = keyof (AsProp<E> & OwnProps);
type PolymorphicProps<E extends React.ElementType> = AsProp<E> & OwnProps & Omit<React.ComponentPropsWithoutRef<E>, PropsToOmit<E>>;
type FragmentPropsGuard<E extends React.ElementType> = E extends typeof React.Fragment ? {
className?: never;
style?: never;
} : {};
/**
* Props for the TreeStream component with polymorphic support.
*
* @template E - The element type for the wrapper component
* @example
* ```tsx
* // Default div wrapper
* <TreeStream>Content</TreeStream>
*
* // Custom element wrapper
* <TreeStream as="section">Content</TreeStream>
*
* // No wrapper (fragment)
* <TreeStream as={React.Fragment}>Content</TreeStream>
* ```
*/
type TreeStreamProps<E extends React.ElementType = 'div'> = PolymorphicProps<E> & FragmentPropsGuard<E>;
/**
* TreeStream - A React component that renders content with a streaming animation effect.
*
* Renders children incrementally over time, creating a typewriter-like effect.
* Supports text streaming (by word or character), instant rendering of React elements,
* and nested TreeStream components with coordinated completion callbacks.
*
* @template E - The element type for the wrapper component (defaults to 'div')
* @param props - The component props
* @param props.as - Optional polymorphic element type. Use React.Fragment for no wrapper
* @param props.children - React nodes to stream. Fragments and arrays are flattened
* @param props.speed - Number of tokens to display per tick (default: 5)
* @param props.interval - Milliseconds between ticks (default: 50)
* @param props.streamBy - Tokenization strategy: 'word' or 'character' (default: 'word')
* @param props.autoStart - Start streaming automatically on mount/change (default: true)
* @param props.onComplete - Callback when streaming completes (including nested streams)
*
* @returns A React element that streams its content progressively
*
* @example
* ```tsx
* // Basic usage
* <TreeStream speed={10} interval={30}>
* Hello world! This text will stream in.
* </TreeStream>
*
* // Character-by-character streaming
* <TreeStream streamBy="character" speed={1}>
* Typing effect...
* </TreeStream>
*
* // With completion callback
* <TreeStream onComplete={() => console.log('Done!')}>
* Content here
* </TreeStream>
*
* // Nested streaming components
* <TreeStream>
* First part
* <TreeStream>Nested content streams after parent</TreeStream>
* Final part
* </TreeStream>
* ```
*/
declare function TreeStream<E extends React.ElementType = 'div'>({ as, children, speed, interval, streamBy, autoStart, onComplete, ...rest }: TreeStreamProps<E>): react_jsx_runtime.JSX.Element;
declare namespace TreeStream {
var displayName: string;
}
/**
* Execution units produced from children to drive the streaming executor.
* - text_stream: a text node that will be tokenized and streamed
* - instant_render: any non-stream Tree element rendered immediately
* - nested_stream: a nested TreeStream element coordinated by onComplete
*/
type ExecutionUnit = {
type: 'text_stream';
content: string;
} | {
type: 'instant_render';
content: React.ReactElement;
} | {
type: 'nested_stream';
component: React.ReactElement;
};
/**
* buildPlan
*
* Convert an arbitrary React node tree into a flat execution plan.
*
* Inputs:
* - node: any React renderable input (string/number/elements/arrays/fragments)
*
* Outputs:
* - Array of ExecutionUnit preserving in-order appearance from the tree
*
* Rules:
* - Strings/numbers become text stream units (empty strings are skipped)
* - Fragments/arrays are flattened recursively
* - Elements marked as TreeStream become nested stream units
* - All other elements are instant render units
*
* Notes/edge cases:
* - null/undefined/boolean nodes are ignored
* - For strings we keep original content (including whitespace), but empty
* strings after trim() are ignored to avoid no-op streaming units
*/
declare function buildPlan(node: React.ReactNode): ExecutionUnit[];
/**
* planSignature
*
* Create a stable string signature for a plan that captures structural shape
* and text content for text units. This is used to decide when to reset/run.
*
* Implementation detail:
* - text_stream includes its content to re-run when text changes
* - instant_render and nested_stream capture only their type (not identity)
*/
declare function planSignature(plan: ExecutionUnit[]): string;
/** Marker symbol placed on the component function to detect wrappers. */
declare const STREAMING_MARKER: unique symbol;
/**
* isTreeStreamElement
*
* Detect whether a React element is a TreeStream component, even if wrapped
* by React.memo or forwardRef. We check the function itself, .type for memo,
* and .render for forwardRef. As a fallback we also check displayName.
*/
declare function isTreeStreamElement(el: React.ReactElement): boolean;
export { type ExecutionUnit, STREAMING_MARKER, TreeStream, type TreeStreamProps, buildPlan, TreeStream as default, isTreeStreamElement, planSignature };