1771 Technologies Logo

Row Data Sources

Server Data Source

This guide shows a basic example of the Controlled Data Source for loading data on an as-needed basis. Before reading this, please ensure you are familiar with controlled data sources in Graphite Grid.

Info

The implementation described below is just one approach to building a server-driven Graphite Grid. Depending on your backend infrastructure, a different approach may make more sense. Graphite Grid provides the flexibility to implement whatever data loading patterns best suit your data.

Caution

The example shown here is designed for clarity rather than production robustness. Network request error handling and data sanitization have been omitted. Use this example as inspiration for what is possible.

Server Endpoint

We start by creating a server endpoint. Our server endpoint will accept POST requests and expect a JSON body of the form:

interface DataRequest {
  rowStart: number;
  rowEnd: number;
}

A hard-coded dataset is used for this example, but the data could come from your database. Here are some sample rows of our data:

const companyData = [
  {
    exchange: "NASDAQ",
    symbol: "ESGR",
    currency: "PHP",
    sector: "Finance",
    industry: "Property-Casualty Insurers",
    price: 74.89,
    volume: null,
  },
  {
    exchange: "NASDAQ",
    symbol: "LSXMA",
    currency: "IDR",
    sector: "Consumer Services",
    industry: null,
    price: 183.03,
    volume: 154810,
  },
  // ... many more rows
];

Finally, we have our REST endpoint. The endpoint simply returns the rows for the given range.

async function POST(req: Request) {
  const body = await req.json();
 
  // Request validation omitted
 
  const { rowStart, rowEnd } = body;
 
  const rows = financeData.slice(rowStart, rowEnd);
  const data = {
    rows,
    rowCount: financeData.length,
  };
 
  const response = new Response(JSON.stringify(data), {
    headers: { "content-type": "application/json" },
    status: 200,
  });
 
  return response;
}

Server-Driven Row Data Source

Next, we need to create a RowDataSourceControlled implementation that we can use to fetch data from our server. Even a basic implementation has quite a bit of request state management. The code below is heavily commented on.

// Define the structure of company data received from the backend
interface CompanyDataRow {
  exchange: string;
  symbol: string;
  currency: string;
  sector: string;
  industry: string;
  price: number;
  volume: number | null;
}
 
// Define the type for a row in the grid
// null is used as a sentinel value to indicate completion of async loading
type Row = CompanyDataRow | null;
 
// Implement a basic Row Data Source for server-side data
class ServerDataSource implements RowDataSourceControlled<Row> {
  // Indicate that this is a controlled data source (required by Graphite Grid)
  kind = "controlled" as const;
 
  // Store loaded row nodes, indexed by row number
  nodes: Record<number, RowNode<Row>>;
 
  // Track the total number of rows in the dataset
  rowCount: number;
 
  // Reference to the Graphite Grid API for refreshing the grid
  api!: GraphiteGridApi<Row>;
 
  // Keep track of which pages of data have been requested
  requestedPages: Set<string>;
 
  // Initialize the row data source
  constructor() {
    this.nodes = {};
    this.rowCount = 0;
    this.requestedPages = new Set();
  }
 
  // Initialize the data source with the Graphite Grid API
  // This method is called by Graphite Grid upon initialization
  init = (api: GraphiteGridApi<Row>) => {
    this.api = api;
 
    // Start by requesting the first page of data
    void this.requestPage(0, 50);
  };
 
  // Fetch a page of data from the server
  // This is a basic implementation and should be enhanced for production use
  requestPage = async (rowStart: number, rowEnd: number) => {
    const page = `${rowStart}-${rowEnd}`;
    if (this.requestedPages.has(page)) return;
 
    // Mark this page as requested to avoid duplicate requests
    this.requestedPages.add(page);
 
    // Fetch data from the server
    const response = await fetch("/api/server-side-data", {
      method: "POST",
      body: JSON.stringify({ rowStart, rowEnd }),
    });
 
    const data = await response.json();
 
    // Process the received data and create row nodes
    (data.rows as CompanyDataRow[]).forEach((row, i) => {
      const rowIndex = i + rowStart;
      const node = createRowNodeLeaf({
        id: `${rowIndex}`,
        data: row,
        rowIndex: rowIndex,
      });
      this.nodes[rowIndex] = node;
    });
 
    // Update the total row count
    this.rowCount = data.rowCount;
 
    // Trigger a grid refresh to reflect the new data
    this.api.refreshRowDataSource();
  };
 
  // Return the total number of rows in the dataset
  getRowCount = () => {
    return this.rowCount;
  };
 
  // Retrieve a row node by its ID
  getRowById = (id: string) => {
    const nodes = Object.values(this.nodes);
    return nodes.find((node) => node.id === id) ?? null;
  };
 
  // Retrieve or create a row node for a given row index
  getRowNode = ({ rowIndex }: GetRowNodeParams<Row>) => {
    let node = this.nodes[rowIndex];
 
    if (!node) {
      // If the node doesn't exist, request the corresponding page of data
      const rowStart = Math.floor(rowIndex / 50) * 50;
      void this.requestPage(rowStart, rowStart + 50);
 
      // Create a placeholder node while data is loading
      // This ensures a stable reference for the row, even before data is available
      node = this.nodes[rowIndex] = createRowNodeLeaf({
        data: null,
        id: `${rowIndex}`,
        isLoading: true,
        rowIndex: rowIndex,
      });
    }
 
    return node;
  };
}

Using the Row Data Source

Once a row data source has been defined, it can be used by Graphite Grid.

export function ServerDataGrid() {
  const grid = useGraphiteGrid<Row>({
    initial: {
      columnDefinitions: financeColumns,
      baseColumnDefinition: {
        freeWidthRatio: 1,
      },
      rowDataSource: new ServerDataSource(),
    },
  });
 
  return (
    <div style={{ height: 300 }}>
      <GraphiteGridDom state={grid} />
    </div>
  );
}

The example above shows a basic setup. It can be extended to implement more functionality, such as:

  • Sorting
  • Pivoting
  • Filtering
  • Row Selection

Tip

All of Graphite Grid's functionality is possible with a controlled data source. There is no trade-off versus a client data source. In fact, Graphite Grid converts the client data source internally to a controlled data source to power the client functionality.