Handling Load Failures
Network requests can fail at any time, that's an inherent part of client-server communication. The LyteNyte Grid server data source provides simple mechanisms to recover from request errors, making it easy to retry failed requests.
Handling Initial Request Failure
When the server data source initializes, it sends an initial data request to the server. If this request fails, LyteNyte Grid can't determine which data slices to re-request because none have loaded. The simplest solution is to reset the grid, as shown in the example below.
Initial Load Error
"use client";import { Grid, useServerDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId, useRef } from "react";import { Server } from "./server";import type { MovieData } from "./data";import {GenreRenderer,LinkRenderer,NameCellRenderer,RatingRenderer,ReleasedRenderer,TypeRenderer,} from "./components";const columns: Column<MovieData>[] = [{id: "#",name: "",width: 30,field: "link",widthMin: 30,widthMax: 30,cellRenderer: LinkRenderer,},{ id: "name", name: "Title", width: 250, widthFlex: 1, cellRenderer: NameCellRenderer },{ id: "released_at", name: "Released", width: 120, cellRenderer: ReleasedRenderer },{ id: "genre", name: "Genre", cellRenderer: GenreRenderer },{ id: "type", name: "Type", width: 120, cellRenderer: TypeRenderer },{ id: "imdb_rating", name: "Rating", width: 120, cellRenderer: RatingRenderer },];export default function ServerDataFailFirst() {// Track a fail ref to simulate network failure but still allow the demo to be reset.const shouldFailRef = useRef(true);const ds = useServerDataSource<MovieData>({dataFetcher: (params) => {const fail = shouldFailRef.current;shouldFailRef.current = false;return Server(params.requests, fail);},blockSize: 50,});const error = ds.loadError.useValue();const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,});const view = grid.view.useValue();return (<div className="lng-grid relative" style={{ height: 500 }}>{!!error && (<div className="absolute left-0 top-0 z-[10] flex h-full w-full flex-col items-center justify-center gap-2 bg-red-500/20"><span>{`${error}`}</span><buttononClick={() => {ds.reset();}}className="border-primary-300 hover:bg-primary-600 bg-primary-500 text-ln-gray-02 cursor-pointer rounded border px-3 py-0.5 text-sm font-semibold">Retry - it will work now</button></div>)}<Grid.Root grid={grid}><Grid.Viewport style={{ overflowY: "scroll" }}><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className="flex h-full w-full items-center px-2 text-sm capitalize"/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainerclassName={ds.isLoading.useValue() ? "animate-pulse bg-gray-100" : ""}><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="flex h-full w-full items-center px-2 text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { CellRendererFn } from "@1771technologies/lytenyte-pro/types";import type { MovieData } from "./data";import { format } from "date-fns";import type { JSX } from "react";import { Rating, ThinRoundedStar } from "@smastrom/react-rating";import "@smastrom/react-rating/style.css";import { Link1Icon } from "@radix-ui/react-icons";function SkeletonLoading() {return (<div className="h-full w-full p-2"><div className="h-full w-full animate-pulse rounded-xl bg-gray-200 dark:bg-gray-100"></div></div>);}export const NameCellRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading) return <SkeletonLoading />;const field = params.grid.api.columnField(params.column, params.row) as string;return <div className="overflow-hidden text-ellipsis">{field}</div>;};export const ReleasedRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading) return <SkeletonLoading />;const field = params.grid.api.columnField(params.column, params.row) as string;const formatted = field ? format(field, "dd MMM yyyy") : "-";return <div>{formatted}</div>;};export const GenreRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading) return <SkeletonLoading />;const field = params.grid.api.columnField(params.column, params.row) as string;const splits = field ? field.split(",") : [];return (<div className="flex h-full w-full items-center gap-1">{splits.map((c) => {return (<divclassName="border-primary-200 text-primary-700 dark:text-primary-500 bg-primary-200/20 rounded border p-1 px-2 text-xs"key={c}>{c}</div>);})}</div>);};const FilmRealIcon = (props: JSX.IntrinsicElements["svg"]) => {return (<svgxmlns="http://www.w3.org/2000/svg"width="20"height="20"fill="currentcolor"viewBox="0 0 256 256"{...props}><path d="M232,216H183.36A103.95,103.95,0,1,0,128,232H232a8,8,0,0,0,0-16ZM40,128a88,88,0,1,1,88,88A88.1,88.1,0,0,1,40,128Zm88-24a24,24,0,1,0-24-24A24,24,0,0,0,128,104Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,128,72Zm24,104a24,24,0,1,0-24,24A24,24,0,0,0,152,176Zm-32,0a8,8,0,1,1,8,8A8,8,0,0,1,120,176Zm56-24a24,24,0,1,0-24-24A24,24,0,0,0,176,152Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,176,120ZM80,104a24,24,0,1,0,24,24A24,24,0,0,0,80,104Zm0,32a8,8,0,1,1,8-8A8,8,0,0,1,80,136Z"></path></svg>);};const MonitorPlayIcon = (props: JSX.IntrinsicElements["svg"]) => {return (<svgxmlns="http://www.w3.org/2000/svg"width="20"height="20"fill="currentcolor"viewBox="0 0 256 256"{...props}><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Zm-3.56-110.66-48-32A8,8,0,0,0,104,88v64a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM120,137.05V103l25.58,17Z"></path></svg>);};export const TypeRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading) return <SkeletonLoading />;const field = params.grid.api.columnField(params.column, params.row) as string;const isMovie = field === "Movie";const Icon = isMovie ? FilmRealIcon : MonitorPlayIcon;return (<div className="flex h-full w-full items-center gap-2"><span className={isMovie ? "text-primary-500" : "text-accent-500"}><Icon /></span><span>{field}</span></div>);};export const RatingRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading) return <SkeletonLoading />;const field = params.grid.api.columnField(params.column, params.row) as string;const rating = field ? Number.parseFloat(field.split("/")[0]) : null;if (rating == null || Number.isNaN(rating)) return "-";return (<div className="flex h-full w-full items-center"><Ratingstyle={{ maxWidth: 100 }}halfFillMode="svg"value={Math.round(rating / 2)}itemStyles={{activeFillColor: "hsla(173, 78%, 34%, 1)",itemShapes: ThinRoundedStar,inactiveFillColor: "transparent",inactiveBoxBorderColor: "transparent",inactiveBoxColor: "transparent",inactiveStrokeColor: "transparent",}}readOnly/></div>);};export const LinkRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading) return <SkeletonLoading />;const field = params.grid.api.columnField(params.column, params.row) as string;return (<a href={field} target="_blank"><Link1Icon /></a>);};
import type { DataRequest, DataResponse } from "@1771technologies/lytenyte-pro/types";import { data } from "./data";const sleep = () => new Promise((res) => setTimeout(res, 600));// Fail the first requestexport async function Server(reqs: DataRequest[], shouldFail: boolean) {// Simulate latency and server work.await sleep();if (shouldFail) {throw new Error("Simulating failed server response");}return reqs.map((c) => {return {asOfTime: Date.now(),data: data.slice(c.start, c.end).map((x) => {return {kind: "leaf",id: x.uniq_id,data: x,};}),start: c.start,end: c.end,kind: "center",path: c.path,size: data.length,} satisfies DataResponse;});}
Clicking the "Retry - it will work now" button calls the reset
method on
the server data source, which resends the initial data request to the server.
Handle Slice Failures
Even after the initial request succeeds, subsequent requests can still fail.
The LyteNyte Grid server data source provides the retry
method to reattempt failed
data requests. This method clears the error state and resends only the failed requests that
are currently in view; failed requests outside the view are skipped but still have their error state cleared.
In the example below, scroll down a few rows to trigger some failed requests. Clicking the "Retry Failed" button clears the error state and re-requests the affected rows. The subsequent requests will succeed.
Failed Row Slice
"use client";import { Grid, useServerDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId, useRef } from "react";import { Server } from "./server";import type { MovieData } from "./data";import {GenreRenderer,LinkRenderer,NameCellRenderer,RatingRenderer,ReleasedRenderer,TypeRenderer,} from "./components";const columns: Column<MovieData>[] = [{id: "#",name: "",width: 30,field: "link",widthMin: 30,widthMax: 30,cellRenderer: LinkRenderer,},{ id: "name", name: "Title", width: 250, widthFlex: 1, cellRenderer: NameCellRenderer },{ id: "released_at", name: "Released", width: 120, cellRenderer: ReleasedRenderer },{ id: "genre", name: "Genre", cellRenderer: GenreRenderer },{ id: "type", name: "Type", width: 120, cellRenderer: TypeRenderer },{ id: "imdb_rating", name: "Rating", width: 120, cellRenderer: RatingRenderer },];export default function FailedSlices() {const shouldFailRef = useRef<Record<string, boolean>>({});const ds = useServerDataSource<MovieData>({dataFetcher: (params) => {return Server(params.requests, shouldFailRef);},blockSize: 50,});const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,});const view = grid.view.useValue();return (<><div className="border-ln-gray-30 flex border-b px-2 py-2"><buttonclassName="border-ln-primary-30 hover:bg-ln-primary-70 bg-ln-primary-50 text-ln-gray-02 cursor-pointer rounded border px-3 py-0.5 text-sm font-semibold"onClick={() => {ds.retry();}}>Retry Failed</button></div><div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport style={{ overflowY: "scroll" }}><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className="flex h-full w-full items-center px-2 text-sm capitalize"/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainerclassName={ds.isLoading.useValue() ? "animate-pulse bg-gray-100" : ""}><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="flex h-full w-full items-center px-2 text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></>);}
import type { CellRendererFn } from "@1771technologies/lytenyte-pro/types";import type { MovieData } from "./data";import { format } from "date-fns";import type { JSX } from "react";import { Rating, ThinRoundedStar } from "@smastrom/react-rating";import "@smastrom/react-rating/style.css";import { Link1Icon } from "@radix-ui/react-icons";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}function SkeletonLoading(props: { error?: boolean }) {return (<div className="h-full w-full p-2"><divclassName={tw("h-full w-full animate-pulse rounded-xl bg-gray-200 dark:bg-gray-100",props.error && "bg-red-400 dark:bg-red-400",)}></div></div>);}export const NameCellRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;const field = params.grid.api.columnField(params.column, params.row) as string;return <div className="overflow-hidden text-ellipsis">{field}</div>;};export const ReleasedRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;const field = params.grid.api.columnField(params.column, params.row) as string;const formatted = field ? format(field, "dd MMM yyyy") : "-";return <div>{formatted}</div>;};export const GenreRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;const field = params.grid.api.columnField(params.column, params.row) as string;const splits = field ? field.split(",") : [];return (<div className="flex h-full w-full items-center gap-1">{splits.map((c) => {return (<divclassName="border-primary-200 text-primary-700 dark:text-primary-500 bg-primary-200/20 rounded border p-1 px-2 text-xs"key={c}>{c}</div>);})}</div>);};const FilmRealIcon = (props: JSX.IntrinsicElements["svg"]) => {return (<svgxmlns="http://www.w3.org/2000/svg"width="20"height="20"fill="currentcolor"viewBox="0 0 256 256"{...props}><path d="M232,216H183.36A103.95,103.95,0,1,0,128,232H232a8,8,0,0,0,0-16ZM40,128a88,88,0,1,1,88,88A88.1,88.1,0,0,1,40,128Zm88-24a24,24,0,1,0-24-24A24,24,0,0,0,128,104Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,128,72Zm24,104a24,24,0,1,0-24,24A24,24,0,0,0,152,176Zm-32,0a8,8,0,1,1,8,8A8,8,0,0,1,120,176Zm56-24a24,24,0,1,0-24-24A24,24,0,0,0,176,152Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,176,120ZM80,104a24,24,0,1,0,24,24A24,24,0,0,0,80,104Zm0,32a8,8,0,1,1,8-8A8,8,0,0,1,80,136Z"></path></svg>);};const MonitorPlayIcon = (props: JSX.IntrinsicElements["svg"]) => {return (<svgxmlns="http://www.w3.org/2000/svg"width="20"height="20"fill="currentcolor"viewBox="0 0 256 256"{...props}><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Zm-3.56-110.66-48-32A8,8,0,0,0,104,88v64a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM120,137.05V103l25.58,17Z"></path></svg>);};export const TypeRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;const field = params.grid.api.columnField(params.column, params.row) as string;const isMovie = field === "Movie";const Icon = isMovie ? FilmRealIcon : MonitorPlayIcon;return (<div className="flex h-full w-full items-center gap-2"><span className={isMovie ? "text-primary-500" : "text-accent-500"}><Icon /></span><span>{field}</span></div>);};export const RatingRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;const field = params.grid.api.columnField(params.column, params.row) as string;const rating = field ? Number.parseFloat(field.split("/")[0]) : null;if (rating == null || Number.isNaN(rating)) return "-";return (<div className="flex h-full w-full items-center"><Ratingstyle={{ maxWidth: 100 }}halfFillMode="svg"value={Math.round(rating / 2)}itemStyles={{activeFillColor: "hsla(173, 78%, 34%, 1)",itemShapes: ThinRoundedStar,inactiveFillColor: "transparent",inactiveBoxBorderColor: "transparent",inactiveBoxColor: "transparent",inactiveStrokeColor: "transparent",}}readOnly/></div>);};export const LinkRenderer: CellRendererFn<MovieData> = (params) => {if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;const field = params.grid.api.columnField(params.column, params.row) as string;return (<a href={field}><Link1Icon /></a>);};
import type { DataRequest, DataResponse } from "@1771technologies/lytenyte-pro/types";import { data } from "./data";const sleep = () => new Promise((res) => setTimeout(res, 600));export async function Server(reqs: DataRequest[], fail: { current: Record<string, boolean> }) {// Simulate latency and server work.await sleep();return reqs.map((c) => {if (c.start > 0 && fail.current[c.id] != false) {fail.current[c.id] = false;throw new Error("Simulate failure");}return {asOfTime: Date.now(),data: data.slice(c.start, c.end).map((x) => {return {kind: "leaf",id: x.uniq_id,data: x,};}),start: c.start,end: c.end,kind: "center",path: c.path,size: data.length,} satisfies DataResponse;});}
Handling Group Failures
The retry
method also handles failed group expansions. Call it to retry a group
expansion that failed, and all associated errored requests will be retried. In the
example below, expanding a group triggers a failure. Clicking the exclamation icon
retries the failed requests, and the subsequent requests succeed.
Group Expansion Failure
"use client";import { Grid, useServerDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId, useRef } from "react";import { Server } from "./server";import type { SalaryData } from "./data";import clsx from "clsx";import { twMerge } from "tailwind-merge";import {AgeCellRenderer,BaseCellRenderer,GroupCellRenderer,SalaryRenderer,YearsOfExperienceRenderer,} from "./components";const columns: Column<SalaryData>[] = [{id: "Gender",width: 120,widthFlex: 1,uiHints: { rowGroupable: true },cellRenderer: BaseCellRenderer,},{id: "Education Level",name: "Education",width: 160,hide: true,widthFlex: 1,uiHints: { rowGroupable: true },cellRenderer: BaseCellRenderer,},{id: "Age",type: "number",width: 100,widthFlex: 1,uiHints: { rowGroupable: true },cellRenderer: AgeCellRenderer,},{id: "Years of Experience",name: "YoE",type: "number",width: 100,widthFlex: 1,cellRenderer: YearsOfExperienceRenderer,uiHints: {rowGroupable: true,},},{ id: "Salary", type: "number", width: 160, widthFlex: 1, cellRenderer: SalaryRenderer },];export default function RowGroupingBasic() {const shouldFailRef = useRef<Record<string, boolean>>({});const ds = useServerDataSource<SalaryData>({dataFetcher: (params) => {return Server(params.requests, params.model.groups, shouldFailRef.current);},blockSize: 50,});const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,rowGroupColumn: {cellRenderer: GroupCellRenderer,widthFlex: 1,},rowGroupModel: ["Education Level"],});const view = grid.view.useValue();return (<><div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport style={{ overflowY: "scroll" }}><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={twMerge(clsx("flex h-full w-full items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",),)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainerclassName={ds.isLoading.useValue() ? "animate-pulse bg-gray-100" : ""}><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={twMerge(clsx("flex h-full w-full items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end tabular-nums",),)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></>);}
import type { CellRendererParams, RowDataSourceServer } from "@1771technologies/lytenyte-pro/types";import type { SalaryData } from "./data";import type { CSSProperties } from "react";import { ChevronDownIcon, ChevronRightIcon } from "@1771technologies/lytenyte-pro/icons";import { tw } from "./ui";import { ExclamationTriangleIcon } from "@radix-ui/react-icons";function SkeletonLoading() {return (<div className="h-full w-full p-2"><div className="h-full w-full animate-pulse rounded-xl bg-gray-200 dark:bg-gray-100"></div></div>);}const formatter = new Intl.NumberFormat("en-Us", {minimumFractionDigits: 0,maximumFractionDigits: 0,});export function SalaryRenderer({ grid, row, column }: CellRendererParams<SalaryData>) {const field = grid.api.columnField(column, row);if (grid.api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;if (typeof field !== "number") return null;return (<div className="flex h-full w-full items-center justify-end">${formatter.format(field)}</div>);}export function YearsOfExperienceRenderer({ grid, row, column }: CellRendererParams<SalaryData>) {const field = grid.api.columnField(column, row);if (grid.api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;if (typeof field !== "number") return null;return (<div className="flex w-full items-baseline justify-end gap-1 tabular-nums"><span className="font-bold">{field}</span>{" "}<span className="text-xs">{field <= 1 ? "Year" : "Years"}</span></div>);}export function AgeCellRenderer({ grid, row, column }: CellRendererParams<SalaryData>) {const field = grid.api.columnField(column, row);if (grid.api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;if (typeof field !== "number") return null;return (<div className="flex w-full items-baseline justify-end gap-1 tabular-nums"><span className="font-bold">{formatter.format(field)}</span>{" "}<span className="text-xs">{field <= 1 ? "Year" : "Years"}</span></div>);}export function BaseCellRenderer({ row, column, grid }: CellRendererParams<any>) {if (grid.api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;const field = grid.api.columnField(column, row);return <div className="flex h-full w-full items-center">{field as string}</div>;}export function GroupCellRenderer({ row, grid }: CellRendererParams<any>) {if (grid.api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;if (grid.api.rowIsLeaf(row)) return <div />;const isExpanded = grid.api.rowGroupIsExpanded(row);const hasError = row.errorGroup;return (<divstyle={{paddingLeft: row.depth * 16,"--before-offset": `${row.depth * 16 - 5}px`,} as CSSProperties}className={tw("relative flex h-full w-full items-center gap-2 overflow-hidden text-nowrap",row.depth > 0 &&"before:border-ln-gray-30 before:absolute before:left-[var(--before-offset)] before:top-0 before:h-full before:border-r before:border-dashed",)}>{row.loadingGroup && (<div className="w-5"><LoadingSpinner /></div>)}{!!hasError && (<buttonclassName="hover:bg-ln-gray-10 w-5 cursor-pointer rounded text-red-400 transition-colors"onClick={() => {(grid.state.rowDataSource.get() as RowDataSourceServer<SalaryData>).retry();}}><span className="sr-only">Toggle the row group</span><ExclamationTriangleIcon /></button>)}{!row.loadingGroup && !hasError && (<buttonclassName="hover:bg-ln-gray-10 w-5 cursor-pointer rounded transition-colors"onClick={() => {grid.api.rowGroupToggle(row);}}><span className="sr-only">Toggle the row group</span>{isExpanded ? <ChevronDownIcon /> : <ChevronRightIcon />}</button>)}<div className="w-full overflow-hidden text-ellipsis">{row.key || "(none)"}</div></div>);}const LoadingSpinner = () => {return (<div className="flex min-h-screen items-center justify-center"><svgclassName="h-4 w-4 animate-spin text-blue-500"xmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"><circleclassName="opacity-25"cx="12"cy="12"r="8"stroke="currentColor"strokeWidth="4"></circle><pathclassName="opacity-75"fill="currentColor"d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path></svg></div>);};
import type {DataRequest,DataResponse,RowGroupModelItem,} from "@1771technologies/lytenyte-pro/types";import type { SalaryData } from "./data";import { data } from "./data";const sleep = () => new Promise((res) => setTimeout(res, 600));export async function Server(reqs: DataRequest[],groupModel: RowGroupModelItem<SalaryData>[],shouldFail: Record<string, boolean>,) {// Simulate latency and server work.await sleep();return reqs.map((c) => {if (c.path.length) {const id = c.path.join("/");if (shouldFail[id] !== false) {shouldFail[id] = false;throw new Error("Simulate failure");}}// Return flat items if there are no row groupsif (!groupModel.length) {return {asOfTime: Date.now(),data: data.slice(c.start, c.end).map((x) => {return {kind: "leaf",id: x.id,data: x,};}),start: c.start,end: c.end,kind: "center",path: c.path,size: data.length,} satisfies DataResponse;}const groupLevel = c.path.length;const groupKeys = groupModel.slice(0, groupLevel + 1);const filteredForGrouping = data.filter((row) => {return c.path.every((v, i) => {const groupKey = groupModel[i];return `${row[groupKey as keyof SalaryData]}` === v;});});// This is the leaf level of the groupingif (groupLevel === groupModel.length) {return {kind: "center",asOfTime: Date.now(),start: c.start,end: c.end,path: c.path,data: filteredForGrouping.slice(c.start, c.end).map((x) => {return {kind: "leaf",id: x.id,data: x,};}),size: filteredForGrouping.length,} satisfies DataResponse;}const groupedData = Object.groupBy(filteredForGrouping, (r) => {const groupPath = groupKeys.map((g) => {if (typeof g !== "string")throw new Error("Non-string groups are not supported by this dummy implementation");return r[g as keyof SalaryData];});return groupPath.join(" / ");});// Sort the groups to make them nicerconst rows = Object.entries(groupedData).sort((x, y) => {const left = x[0];const right = y[0];const asNumberLeft = Number.parseFloat(left);const asNumberRight = Number.parseFloat(right);if (Number.isNaN(asNumberLeft) || Number.isNaN(asNumberRight)) {if (!left && !right) return 0;if (!left) return 1;if (!right) return -1;return left.localeCompare(right);}return asNumberLeft - asNumberRight;});return {kind: "center",asOfTime: Date.now(),data: rows.slice(c.start, c.end).map((x) => {const childRows = x[1]!;const nextGroup = groupLevel + 1;let childCnt: number;if (nextGroup === groupModel.length) childCnt = childRows.length;else {childCnt = Object.keys(Object.groupBy(childRows, (x) => {const groupKey = groupModel[nextGroup];return x[groupKey as keyof SalaryData];}),).length;}return {kind: "branch",childCount: childCnt,data: {}, // See aggregationsid: x[0],key: x[0].split(" / ").at(-1)!,};}),path: c.path,start: c.start,end: c.end,size: rows.length,} satisfies DataResponse;});}
The retry
method is invoked in the GroupCellRenderer
button component:
<buttononClick={() => {(grid.state.rowDataSource.get() as RowDataSourceServer<SalaryData>).retry();}}><ExclamationTriangleIcon /></button>
The TypeScript cast is required because the server data source is a subtype of the general row data source.
Next Steps
- Server Row Data: shows how to fetch server data in slices.
- Server Row Grouping and Aggregation: handle grouped data and load group slices.
- Optimistic Loading: explores LyteNyte Grid's optimistic loading for pre-fetching data, providing a responsive client-side experience.
Data Pushing or Pulling
The LyteNyte Grid's server data source enables programmatic data exchange with the server, giving developers full control over how data is loaded into the grid. It also supports hybrid patterns where row data originates from both the client and the server.
Column Manager
Learn how to implement and customize the Column Manager component to give users control over column visibility, grouping, aggregations, and pivots in LyteNyte Grid PRO.