CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-virtualized

React components for efficiently rendering large lists and tabular data

Pending
Overview
Eval results
Files

dynamic-content.mddocs/

Dynamic Content and Measurement

Components for handling dynamic content sizing, measurement, and infinite loading scenarios.

Capabilities

CellMeasurer Component

Measures dynamic cell content to provide accurate dimensions for virtualization, essential for variable-height content.

/**
 * Measures dynamic cell content for accurate virtualization
 * @param props - CellMeasurer configuration
 */
function CellMeasurer(props: {
  /** Cache instance for storing measurements */
  cache: CellMeasurerCache;
  /** Function that renders the measurable content */
  children: (params: {measure: () => void, registerChild: (element: HTMLElement) => void}) => React.Node;
  /** Column index (for Grid components) */
  columnIndex?: number;
  /** Parent component reference */
  parent: React.Component;
  /** Row index */
  rowIndex: number;
}): React.Component;

CellMeasurerCache Class

Cache for storing cell measurements to optimize performance and avoid re-measuring cells.

/**
 * Cache for CellMeasurer measurements
 */
class CellMeasurerCache {
  /**
   * Creates a new measurement cache
   * @param params - Cache configuration
   */
  constructor(params: {
    /** Default height for unmeasured cells */
    defaultHeight?: number;
    /** Default width for unmeasured cells */
    defaultWidth?: number;
    /** Whether all cells have fixed height */
    fixedHeight?: boolean;
    /** Whether all cells have fixed width */
    fixedWidth?: boolean;
    /** Minimum height for any cell */
    minHeight?: number;
    /** Minimum width for any cell */
    minWidth?: number;
    /** Function to generate cache keys */
    keyMapper?: (rowIndex: number, columnIndex: number) => string;
  });

  /** Clear cached measurements for a specific cell */
  clear(rowIndex: number, columnIndex?: number): void;

  /** Clear all cached measurements */
  clearAll(): void;

  /** Get cached height for a cell */
  getHeight(rowIndex: number, columnIndex?: number): number;

  /** Get cached width for a cell */
  getWidth(rowIndex: number, columnIndex?: number): number;

  /** Check if a cell has been measured */
  has(rowIndex: number, columnIndex?: number): boolean;

  /** Check if cache uses fixed height */
  hasFixedHeight(): boolean;

  /** Check if cache uses fixed width */
  hasFixedWidth(): boolean;

  /** Row height function for List/Grid components */
  rowHeight(params: {index: number}): number;

  /** Column width function for Grid components */
  columnWidth(params: {index: number}): number;

  /** Set cached dimensions for a cell */
  set(rowIndex: number, columnIndex: number, width: number, height: number): void;

  /** Default height value */
  get defaultHeight(): number;

  /** Default width value */
  get defaultWidth(): number;
}

Usage Examples:

import React from 'react';
import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';

// Dynamic height list with CellMeasurer
function DynamicHeightList({ items }) {
  const cache = new CellMeasurerCache({
    fixedWidth: true,
    defaultHeight: 100
  });

  const rowRenderer = ({ index, key, parent, style }) => (
    <CellMeasurer
      cache={cache}
      columnIndex={0}
      key={key}
      parent={parent}
      rowIndex={index}
    >
      <div style={style} className="dynamic-row">
        <h3>{items[index].title}</h3>
        <p>{items[index].description}</p>
        <div className="tags">
          {items[index].tags.map(tag => (
            <span key={tag} className="tag">{tag}</span>
          ))}
        </div>
      </div>
    </CellMeasurer>
  );

  return (
    <List
      deferredMeasurementCache={cache}
      height={600}
      rowCount={items.length}
      rowHeight={cache.rowHeight}
      rowRenderer={rowRenderer}
      width={400}
    />
  );
}

// Dynamic grid with variable cell sizes
function DynamicGrid({ data }) {
  const cache = new CellMeasurerCache({
    defaultHeight: 80,
    defaultWidth: 120,
    fixedHeight: false,
    fixedWidth: false
  });

  const cellRenderer = ({ columnIndex, key, parent, rowIndex, style }) => (
    <CellMeasurer
      cache={cache}
      columnIndex={columnIndex}
      key={key}
      parent={parent}
      rowIndex={rowIndex}
    >
      <div style={style} className="dynamic-cell">
        <div className="cell-content">
          {data[rowIndex][columnIndex]}
        </div>
      </div>
    </CellMeasurer>
  );

  return (
    <Grid
      cellRenderer={cellRenderer}
      columnCount={data[0].length}
      columnWidth={cache.columnWidth}
      deferredMeasurementCache={cache}
      height={500}
      rowCount={data.length}
      rowHeight={cache.rowHeight}
      width={800}
    />
  );
}

// List with image content requiring measurement
function ImageList({ posts }) {
  const cache = new CellMeasurerCache({
    fixedWidth: true,
    minHeight: 200
  });

  const rowRenderer = ({ index, key, parent, style }) => {
    const post = posts[index];
    
    return (
      <CellMeasurer
        cache={cache}
        columnIndex={0}
        key={key}
        parent={parent}
        rowIndex={index}
      >
        {({ measure, registerChild }) => (
          <div ref={registerChild} style={style} className="post-item">
            <img
              src={post.imageUrl}
              alt={post.title}
              onLoad={measure}
              style={{ width: '100%', height: 'auto' }}
            />
            <div className="post-content">
              <h3>{post.title}</h3>
              <p>{post.excerpt}</p>
            </div>
          </div>
        )}
      </CellMeasurer>
    );
  };

  return (
    <List
      deferredMeasurementCache={cache}
      height={600}
      rowCount={posts.length}
      rowHeight={cache.rowHeight}
      rowRenderer={rowRenderer}
      width={400}
    />
  );
}

InfiniteLoader Component

Manages loading additional data as the user scrolls, perfect for implementing infinite scroll functionality.

/**
 * Manages loading additional data as user scrolls
 * @param props - InfiniteLoader configuration
 */
function InfiniteLoader(props: {
  /** Function that renders the scrollable component */
  children: (params: {
    onRowsRendered: (params: {overscanStartIndex: number, overscanStopIndex: number, startIndex: number, stopIndex: number}) => void,
    registerChild: (element: React.Component) => void
  }) => React.Node;
  /** Function to check if a row is loaded */
  isRowLoaded: (params: {index: number}) => boolean;
  /** Function to load more rows */
  loadMoreRows: (params: {startIndex: number, stopIndex: number}) => Promise<any>;
  /** Total number of rows (including unloaded) */
  rowCount: number;
  /** Minimum number of rows to batch load (default: 10) */
  minimumBatchSize?: number;
  /** Number of rows to look ahead for loading (default: 15) */
  threshold?: number;
}): React.Component;

Usage Examples:

import React, { useState, useCallback } from 'react';
import { InfiniteLoader, List, AutoSizer } from 'react-virtualized';

// Basic infinite loading list
function InfiniteList() {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);

  const isRowLoaded = ({ index }) => {
    return !!items[index];
  };

  const loadMoreRows = useCallback(async ({ startIndex, stopIndex }) => {
    if (loading) return;
    
    setLoading(true);
    try {
      // Simulate API call
      const newItems = await fetchItems(startIndex, stopIndex);
      setItems(prevItems => {
        const updatedItems = [...prevItems];
        newItems.forEach((item, index) => {
          updatedItems[startIndex + index] = item;
        });
        return updatedItems;
      });
    } finally {
      setLoading(false);
    }
  }, [loading]);

  const rowRenderer = ({ index, key, style }) => {
    const item = items[index];
    
    if (!item) {
      return (
        <div key={key} style={style} className="loading-row">
          Loading...
        </div>
      );
    }

    return (
      <div key={key} style={style} className="item-row">
        <h4>{item.title}</h4>
        <p>{item.description}</p>
      </div>
    );
  };

  return (
    <div style={{ height: 400, width: '100%' }}>
      <InfiniteLoader
        isRowLoaded={isRowLoaded}
        loadMoreRows={loadMoreRows}
        rowCount={10000} // Total possible rows
        threshold={15}
      >
        {({ onRowsRendered, registerChild }) => (
          <AutoSizer>
            {({ height, width }) => (
              <List
                ref={registerChild}
                height={height}
                width={width}
                rowCount={10000}
                rowHeight={80}
                rowRenderer={rowRenderer}
                onRowsRendered={onRowsRendered}
              />
            )}
          </AutoSizer>
        )}
      </InfiniteLoader>
    </div>
  );
}

// Advanced infinite loader with error handling
function AdvancedInfiniteList({ apiEndpoint }) {
  const [items, setItems] = useState([]);
  const [errors, setErrors] = useState({});
  const [hasNextPage, setHasNextPage] = useState(true);

  const isRowLoaded = ({ index }) => {
    return !!items[index] || !!errors[index];
  };

  const loadMoreRows = async ({ startIndex, stopIndex }) => {
    // Don't load if we've reached the end
    if (!hasNextPage && startIndex >= items.length) {
      return;
    }

    try {
      const response = await fetch(
        `${apiEndpoint}?start=${startIndex}&count=${stopIndex - startIndex + 1}`
      );
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const data = await response.json();
      
      setItems(prevItems => {
        const updatedItems = [...prevItems];
        data.items.forEach((item, index) => {
          updatedItems[startIndex + index] = item;
        });
        return updatedItems;
      });

      setHasNextPage(data.hasMore);
      
      // Clear any previous errors for this range
      setErrors(prevErrors => {
        const updatedErrors = { ...prevErrors };
        for (let i = startIndex; i <= stopIndex; i++) {
          delete updatedErrors[i];
        }
        return updatedErrors;
      });

    } catch (error) {
      // Mark these indices as having errors
      setErrors(prevErrors => {
        const updatedErrors = { ...prevErrors };
        for (let i = startIndex; i <= stopIndex; i++) {
          updatedErrors[i] = error.message;
        }
        return updatedErrors;
      });
    }
  };

  const rowRenderer = ({ index, key, style }) => {
    const item = items[index];
    const error = errors[index];

    if (error) {
      return (
        <div key={key} style={style} className="error-row">
          Error loading item: {error}
          <button onClick={() => loadMoreRows({ startIndex: index, stopIndex: index })}>
            Retry
          </button>
        </div>
      );
    }

    if (!item) {
      return (
        <div key={key} style={style} className="loading-row">
          <div className="spinner" />
          Loading...
        </div>
      );
    }

    return (
      <div key={key} style={style} className="item-row">
        <img src={item.thumbnail} alt="" />
        <div className="item-content">
          <h4>{item.title}</h4>
          <p>{item.description}</p>
          <small>ID: {item.id}</small>
        </div>
      </div>
    );
  };

  const estimatedRowCount = hasNextPage ? items.length + 100 : items.length;

  return (
    <div style={{ height: 500, width: '100%' }}>
      <InfiniteLoader
        isRowLoaded={isRowLoaded}
        loadMoreRows={loadMoreRows}
        rowCount={estimatedRowCount}
        minimumBatchSize={20}
        threshold={10}
      >
        {({ onRowsRendered, registerChild }) => (
          <AutoSizer>
            {({ height, width }) => (
              <List
                ref={registerChild}
                height={height}
                width={width}
                rowCount={estimatedRowCount}
                rowHeight={100}
                rowRenderer={rowRenderer}
                onRowsRendered={onRowsRendered}
              />
            )}
          </AutoSizer>
        )}
      </InfiniteLoader>
    </div>
  );
}

// Helper function to simulate API calls
async function fetchItems(startIndex, stopIndex) {
  // Simulate network delay
  await new Promise(resolve => setTimeout(resolve, 500));
  
  const count = stopIndex - startIndex + 1;
  return Array.from({ length: count }, (_, i) => ({
    id: startIndex + i,
    title: `Item ${startIndex + i}`,
    description: `Description for item ${startIndex + i}`,
    thumbnail: `https://picsum.photos/60/60?random=${startIndex + i}`
  }));
}

Install with Tessl CLI

npx tessl i tessl/npm-react-virtualized

docs

core-components.md

dynamic-content.md

index.md

layout-components.md

navigation-components.md

specialized-layouts.md

table-components.md

utilities.md

tile.json