Handling Load Failures
Use the server data source's built-in error recovery mechanisms to manage and retry failed network requests.
Handling Initial Request Failure
When the server data source initializes, it sends an initial data request. If this fails, LyteNyte Grid can’t retry specific slices because none have loaded. To recover, reset the grid as demonstrated below:
Initial Load Error
1"use client";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { Grid, useServerDataSource } from "@1771technologies/lytenyte-pro";5
6import { Server } from "./server.js";7import type { MovieData } from "./data";8import {9 GenreRenderer,10 LinkRenderer,11 NameCellRenderer,12 RatingRenderer,13 ReleasedRenderer,14 TypeRenderer,15} from "./components.js";16import { useMemo, useRef } from "react";17
18export interface GridSpec {19 readonly data: MovieData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 {24 id: "#",25 name: "",26 width: 30,27 field: "link",28 widthMin: 30,29 widthMax: 30,30 cellRenderer: LinkRenderer,31 },32 { id: "name", name: "Title", width: 250, widthFlex: 1, cellRenderer: NameCellRenderer },33 { id: "released_at", name: "Released", width: 120, cellRenderer: ReleasedRenderer },34 { id: "genre", name: "Genre", cellRenderer: GenreRenderer },35 { id: "type", name: "Type", width: 120, cellRenderer: TypeRenderer },36 { id: "imdb_rating", name: "Rating", width: 120, cellRenderer: RatingRenderer },37];38export default function ServerDataDemo() {39 // Track a fail ref to simulate network failure but still allow the demo to be reset.40 const shouldFailRef = useRef(true);41 const ds = useServerDataSource<MovieData>({42 queryFn: (params) => {43 const fail = shouldFailRef.current;44 shouldFailRef.current = false;45 return Server(params.requests, fail);46 },47 queryKey: [],48 blockSize: 50,49 });50 const error = ds.loadingError.useValue();51 const isLoading = ds.isLoading.useValue();52
53 return (54 <div className="ln-grid relative" style={{ height: 500 }}>55 {!!error && (56 <div className="z-2 absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center gap-2 bg-red-500/20">57 <span>{`${error}`}</span>58 <button59 data-ln-button="website"60 data-ln-size="md"61 onClick={() => {62 ds.reset();63 }}64 >65 Retry / Resolve66 </button>67 </div>68 )}69
70 <Grid71 rowSource={ds}72 columns={columns}73 styles={useMemo(() => {74 return { viewport: { style: { overflowY: "scroll" } } };75 }, [])}76 slotViewportOverlay={77 isLoading && (78 <div className="bg-ln-gray-20/40 absolute left-0 top-0 z-20 h-full w-full animate-pulse"></div>79 )80 }81 />82 </div>83 );84}1import { format } from "date-fns";2import type { JSX } from "react";3import { Rating, ThinRoundedStar } from "@smastrom/react-rating";4import "@smastrom/react-rating/style.css";5import { Link1Icon } from "@radix-ui/react-icons";6import type { Grid } from "@1771technologies/lytenyte-pro";7import type { GridSpec } from "./demo";8
9function SkeletonLoading() {10 return (11 <div className="h-full w-full p-2">12 <div className="bg-ln-gray-20 h-full w-full animate-pulse rounded-xl"></div>13 </div>14 );15}16
17export const NameCellRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {18 if (params.row.loading && !params.row.data) return <SkeletonLoading />;19
20 const field = params.api.columnField(params.column, params.row) as string;21
22 return <div className="overflow-hidden text-ellipsis">{field}</div>;23};24
25export const ReleasedRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {26 if (params.row.loading && !params.row.data) return <SkeletonLoading />;27 const field = params.api.columnField(params.column, params.row) as string;28
29 const formatted = field ? format(field, "dd MMM yyyy") : "-";30
31 return <div>{formatted}</div>;32};33
34export const GenreRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {35 if (params.row.loading && !params.row.data) return <SkeletonLoading />;36 const field = params.api.columnField(params.column, params.row) as string;37
38 const splits = field ? field.split(",") : [];39
40 return (41 <div className="flex h-full w-full items-center gap-1">42 {splits.map((c) => {43 return (44 <div45 className="border-(--primary-200) text-(--primary-700) dark:text-(--primary-500) bg-(--primary-200)/20 rounded border p-1 px-2 text-xs"46 key={c}47 >48 {c}49 </div>50 );51 })}52 </div>53 );54};55
56const FilmRealIcon = (props: JSX.IntrinsicElements["svg"]) => {57 return (58 <svg59 xmlns="http://www.w3.org/2000/svg"60 width="20"61 height="20"62 fill="currentcolor"63 viewBox="0 0 256 256"64 {...props}65 >66 <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>67 </svg>68 );69};70
71const MonitorPlayIcon = (props: JSX.IntrinsicElements["svg"]) => {72 return (73 <svg74 xmlns="http://www.w3.org/2000/svg"75 width="20"76 height="20"77 fill="currentcolor"78 viewBox="0 0 256 256"79 {...props}80 >81 <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>82 </svg>83 );84};85
86export const TypeRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {87 if (params.row.loading && !params.row.data) return <SkeletonLoading />;88 const field = params.api.columnField(params.column, params.row) as string;89
90 const isMovie = field === "Movie";91 const Icon = isMovie ? FilmRealIcon : MonitorPlayIcon;92
93 return (94 <div className="flex h-full w-full items-center gap-2">95 <span className={isMovie ? "text-(--primary-500)" : "text-ln-primary-50"}>96 <Icon />97 </span>98 <span>{field}</span>99 </div>100 );101};102
103export const RatingRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {104 if (params.row.loading && !params.row.data) return <SkeletonLoading />;105 const field = params.api.columnField(params.column, params.row) as string;106 const rating = field ? Number.parseFloat(field.split("/")[0]) : null;107 if (rating == null || Number.isNaN(rating)) return "-";108
109 return (110 <div className="flex h-full w-full items-center">111 <Rating112 style={{ maxWidth: 100 }}113 halfFillMode="svg"114 value={Math.round(rating / 2)}115 itemStyles={{116 activeFillColor: "hsla(173, 78%, 34%, 1)",117 itemShapes: ThinRoundedStar,118 inactiveFillColor: "transparent",119 inactiveBoxBorderColor: "transparent",120 inactiveBoxColor: "transparent",121 inactiveStrokeColor: "transparent",122 }}123 readOnly124 />125 </div>126 );127};128
129export const LinkRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {130 if (params.row.loading && !params.row.data) return <SkeletonLoading />;131 const field = params.api.columnField(params.column, params.row) as string;132
133 return (134 <a href={field} className="text-(--primary-500)">135 <Link1Icon />136 </a>137 );138};1import type { DataRequest, DataResponse } from "@1771technologies/lytenyte-pro";2import { data } from "./data.js";3
4const sleep = () => new Promise((res) => setTimeout(res, 600));5
6// Fail the first request7
8export async function Server(reqs: DataRequest[], shouldFail: boolean) {9 // Simulate latency and server work.10 await sleep();11
12 if (shouldFail) {13 throw new Error("Simulating failed server response");14 }15
16 return reqs.map((c) => {17 return {18 asOfTime: Date.now(),19 data: data.slice(c.start, c.end).map((x) => {20 return {21 kind: "leaf",22 id: x.uniq_id,23 data: x,24 };25 }),26 start: c.start,27 end: c.end,28 kind: "center",29 path: c.path,30 size: data.length,31 } satisfies DataResponse;32 });33}Clicking the Retry / Resolve 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 following demo, scroll down to trigger failed requests. Click the Retry Failed button to clear the error state and successfully re-request the affected rows.
Failed Row Slice
1"use client";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { Grid, useServerDataSource } from "@1771technologies/lytenyte-pro";5
6import { useMemo, useRef } from "react";7import { Server } from "./server.js";8import type { MovieData } from "./data";9import {10 GenreRenderer,11 LinkRenderer,12 NameCellRenderer,13 RatingRenderer,14 ReleasedRenderer,15 TypeRenderer,16} from "./components.js";17
18export interface GridSpec {19 readonly data: MovieData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 {24 id: "#",25 name: "",26 width: 30,27 field: "link",28 widthMin: 30,29 widthMax: 30,30 cellRenderer: LinkRenderer,31 },32 { id: "name", name: "Title", width: 250, widthFlex: 1, cellRenderer: NameCellRenderer },33 { id: "released_at", name: "Released", width: 120, cellRenderer: ReleasedRenderer },34 { id: "genre", name: "Genre", cellRenderer: GenreRenderer },35 { id: "type", name: "Type", width: 120, cellRenderer: TypeRenderer },36 { id: "imdb_rating", name: "Rating", width: 120, cellRenderer: RatingRenderer },37];38
39export default function ServerDataDemo() {40 const shouldFailRef = useRef<Record<string, boolean>>({});41 const ds = useServerDataSource<GridSpec["data"], []>({42 queryFn: async (params) => {43 const x = await Server(params.requests, shouldFailRef);44 return x;45 },46 queryKey: [],47 blockSize: 50,48 });49
50 const isLoading = ds.isLoading.useValue();51
52 return (53 <>54 <div className="px-2 py-1">55 <button56 data-ln-button="website"57 data-ln-size="md"58 onClick={() => {59 ds.retry();60 }}61 >62 Retry Failed63 </button>64 </div>65 <div className="ln-grid" style={{ height: 500 }}>66 <Grid67 rowSource={ds}68 columns={columns}69 styles={useMemo(() => {70 return { viewport: { style: { scrollbarGutter: "stable" } } };71 }, [])}72 slotViewportOverlay={73 isLoading && (74 <div className="bg-ln-gray-20/40 absolute left-0 top-0 z-20 h-full w-full animate-pulse"></div>75 )76 }77 />78 </div>79 </>80 );81}1import { format } from "date-fns";2import type { JSX } from "react";3import { Rating, ThinRoundedStar } from "@smastrom/react-rating";4import "@smastrom/react-rating/style.css";5import { Link1Icon } from "@radix-ui/react-icons";6import type { ClassValue } from "clsx";7import clsx from "clsx";8import { twMerge } from "tailwind-merge";9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11
12function tw(...c: ClassValue[]) {13 return twMerge(clsx(...c));14}15
16function SkeletonLoading(props: { error?: boolean }) {17 return (18 <div className="h-full w-full p-2">19 <div20 className={tw(21 "bg-ln-gray-20 h-full w-full animate-pulse rounded-xl",22 props.error && "bg-ln-red-50/50",23 )}24 ></div>25 </div>26 );27}28
29export const NameCellRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {30 if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;31
32 const field = params.api.columnField(params.column, params.row) as string;33
34 return <div className="overflow-hidden text-ellipsis">{field}</div>;35};36
37export const ReleasedRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {38 if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;39 const field = params.api.columnField(params.column, params.row) as string;40
41 const formatted = field ? format(field, "dd MMM yyyy") : "-";42
43 return <div>{formatted}</div>;44};45
46export const GenreRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {47 if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;48 const field = params.api.columnField(params.column, params.row) as string;49
50 const splits = field ? field.split(",") : [];51
52 return (53 <div className="flex h-full w-full items-center gap-1">54 {splits.map((c) => {55 return (56 <div57 className="border-(--primary-200) text-(--primary-700) dark:text-(--primary-500) bg-(--primary-200)/20 rounded border p-1 px-2 text-xs"58 key={c}59 >60 {c}61 </div>62 );63 })}64 </div>65 );66};67
68const FilmRealIcon = (props: JSX.IntrinsicElements["svg"]) => {69 return (70 <svg71 xmlns="http://www.w3.org/2000/svg"72 width="20"73 height="20"74 fill="currentcolor"75 viewBox="0 0 256 256"76 {...props}77 >78 <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>79 </svg>80 );81};82
83const MonitorPlayIcon = (props: JSX.IntrinsicElements["svg"]) => {84 return (85 <svg86 xmlns="http://www.w3.org/2000/svg"87 width="20"88 height="20"89 fill="currentcolor"90 viewBox="0 0 256 256"91 {...props}92 >93 <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>94 </svg>95 );96};97
98export const TypeRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {99 if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;100 const field = params.api.columnField(params.column, params.row) as string;101
102 const isMovie = field === "Movie";103 const Icon = isMovie ? FilmRealIcon : MonitorPlayIcon;104
105 return (106 <div className="flex h-full w-full items-center gap-2">107 <span className={isMovie ? "text-(--primary-500)" : "text-ln-primary-50"}>108 <Icon />109 </span>110 <span>{field}</span>111 </div>112 );113};114
115export const RatingRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {116 if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;117
118 const field = params.api.columnField(params.column, params.row) as string;119 const rating = field ? Number.parseFloat(field.split("/")[0]) : null;120 if (rating == null || Number.isNaN(rating)) return "-";121
122 return (123 <div className="flex h-full w-full items-center">124 <Rating125 style={{ maxWidth: 100 }}126 halfFillMode="svg"127 value={Math.round(rating / 2)}128 itemStyles={{129 activeFillColor: "hsla(173, 78%, 34%, 1)",130 itemShapes: ThinRoundedStar,131 inactiveFillColor: "transparent",132 inactiveBoxBorderColor: "transparent",133 inactiveBoxColor: "transparent",134 inactiveStrokeColor: "transparent",135 }}136 readOnly137 />138 </div>139 );140};141
142export const LinkRenderer = (params: Grid.T.CellRendererParams<GridSpec>) => {143 if (params.row.loading || params.row.error) return <SkeletonLoading error={!!params.row.error} />;144
145 const field = params.api.columnField(params.column, params.row) as string;146
147 return (148 <a href={field} className="text-(--primary-500)">149 <Link1Icon />150 </a>151 );152};1import type { DataRequest, DataResponse } from "@1771technologies/lytenyte-pro";2import { data } from "./data.js";3
4const sleep = () => new Promise((res) => setTimeout(res, 600));5
6export async function Server(reqs: DataRequest[], fail: { current: Record<string, boolean> }) {7 // Simulate latency and server work.8 await sleep();9
10 return reqs.map((c) => {11 if (c.start > 0 && fail.current[c.id] != false) {12 fail.current[c.id] = false;13 throw new Error("Simulate failure");14 }15
16 return {17 asOfTime: Date.now(),18 data: data.slice(c.start, c.end).map((x) => {19 return {20 kind: "leaf",21 id: x.uniq_id,22 data: x,23 };24 }),25 start: c.start,26 end: c.end,27 kind: "center",28 path: c.path,29 size: data.length,30 } satisfies DataResponse;31 });32}Handling Group Failures
The retry method also recovers failed group expansions. Calling it re-requests all errored
fetches associated with that group. In the demo below, expand a group to trigger
an intentional failure, then click the exclamation icon to successfully retry the request.
Group Expansion Failure
1import "@1771technologies/lytenyte-pro/components.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import { Grid, useServerDataSource } from "@1771technologies/lytenyte-pro";4
5import { Server } from "./server.js";6import type { SalaryData } from "./data";7import {8 AgeCellRenderer,9 BaseCellRenderer,10 SalaryRenderer,11 YearsOfExperienceRenderer,12} from "./components.js";13import { useRef } from "react";14import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";15
16export interface GridSpec {17 readonly data: SalaryData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 {22 id: "Gender",23 width: 120,24 widthFlex: 1,25 cellRenderer: BaseCellRenderer,26 },27 {28 id: "Education Level",29 name: "Education",30 width: 160,31 hide: true,32 widthFlex: 1,33 cellRenderer: BaseCellRenderer,34 },35 {36 id: "Age",37 type: "number",38 width: 100,39 widthFlex: 1,40 cellRenderer: AgeCellRenderer,41 },42 {43 id: "Years of Experience",44 name: "YoE",45 type: "number",46 width: 100,47 widthFlex: 1,48 cellRenderer: YearsOfExperienceRenderer,49 },50 { id: "Salary", type: "number", width: 160, widthFlex: 1, cellRenderer: SalaryRenderer },51];52
53const group: Grid.RowGroupColumn<GridSpec> = {54 cellRenderer: RowGroupCell,55 width: 200,56 pin: "start",57};58
59export default function ServerDataDemo() {60 const shouldFailRef = useRef<Record<string, boolean>>({});61
62 const ds = useServerDataSource({63 queryFn: (params) => {64 return Server(params.requests, ["Education Level"], shouldFailRef.current);65 },66 hasRowBranches: true,67 queryKey: [],68 blockSize: 50,69 });70
71 const isLoading = ds.isLoading.useValue();72
73 return (74 <div className="ln-grid" style={{ height: 500 }}>75 <Grid76 rowSource={ds}77 columns={columns}78 rowGroupColumn={group}79 slotViewportOverlay={80 isLoading && (81 <div className="bg-ln-gray-20/40 absolute left-0 top-0 z-20 h-full w-full animate-pulse"></div>82 )83 }84 />85 </div>86 );87}1import type { Grid } from "@1771technologies/lytenyte-pro";2import { twMerge } from "tailwind-merge";3import clsx, { type ClassValue } from "clsx";4import type { GridSpec } from "./demo";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9
10function SkeletonLoading() {11 return (12 <div className="h-full w-full p-2">13 <div className="h-full w-full animate-pulse rounded-xl bg-gray-200 dark:bg-gray-100"></div>14 </div>15 );16}17
18const formatter = new Intl.NumberFormat("en-Us", {19 minimumFractionDigits: 0,20 maximumFractionDigits: 0,21});22export function SalaryRenderer({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {23 const field = api.columnField(column, row);24
25 if (api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;26
27 if (typeof field !== "number") return "-";28
29 return <div className="flex h-full w-full items-center justify-end">${formatter.format(field)}</div>;30}31
32export function YearsOfExperienceRenderer({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {33 const field = api.columnField(column, row);34
35 if (api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;36
37 if (typeof field !== "number") return "-";38
39 return (40 <div className="flex w-full items-baseline justify-end gap-1 tabular-nums">41 <span className="font-bold">{field}</span>{" "}42 <span className="text-xs">{field <= 1 ? "Year" : "Years"}</span>43 </div>44 );45}46
47export function AgeCellRenderer({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {48 const field = api.columnField(column, row);49
50 if (api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;51
52 if (typeof field !== "number") return "-";53
54 return (55 <div className="flex w-full items-baseline justify-end gap-1 tabular-nums">56 <span className="font-bold">{formatter.format(field)}</span>{" "}57 <span className="text-xs">{field <= 1 ? "Year" : "Years"}</span>58 </div>59 );60}61
62export function BaseCellRenderer({ row, column, api }: Grid.T.CellRendererParams<GridSpec>) {63 if (api.rowIsLeaf(row) && row.loading) return <SkeletonLoading />;64
65 const field = api.columnField(column, row);66
67 return <div className="flex h-full w-full items-center">{(field as string) ?? "-"}</div>;68}1import type { DataRequest, DataResponse } from "@1771technologies/lytenyte-pro";2import type { SalaryData } from "./data";3import { data } from "./data.js";4
5const sleep = () => new Promise((res) => setTimeout(res, 600));6
7export async function Server(reqs: DataRequest[], groupModel: string[], shouldFail: Record<string, boolean>) {8 // Simulate latency and server work.9 await sleep();10
11 return reqs.map((c) => {12 if (c.path.length) {13 const id = c.path.join("/");14 if (shouldFail[id] !== false) {15 shouldFail[id] = false;16 throw new Error("Simulate failure");17 }18 }19
20 // Return flat items if there are no row groups21 if (!groupModel.length) {22 return {23 asOfTime: Date.now(),24 data: data.slice(c.start, c.end).map((x) => {25 return {26 kind: "leaf",27 id: x.id,28 data: x,29 };30 }),31 start: c.start,32 end: c.end,33 kind: "center",34 path: c.path,35 size: data.length,36 } satisfies DataResponse;37 }38
39 const groupLevel = c.path.length;40 const groupKeys = groupModel.slice(0, groupLevel + 1);41
42 const filteredForGrouping = data.filter((row) => {43 return c.path.every((v, i) => {44 const groupKey = groupModel[i];45 return `${row[groupKey as keyof SalaryData]}` === v;46 });47 });48
49 // This is the leaf level of the grouping50 if (groupLevel === groupModel.length) {51 return {52 kind: "center",53 asOfTime: Date.now(),54 start: c.start,55 end: c.end,56 path: c.path,57 data: filteredForGrouping.slice(c.start, c.end).map((x) => {58 return {59 kind: "leaf",60 id: x.id,61 data: x,62 };63 }),64 size: filteredForGrouping.length,65 } satisfies DataResponse;66 }67
68 const groupedData = Object.groupBy(filteredForGrouping, (r) => {69 const groupPath = groupKeys.map((g) => {70 if (typeof g !== "string")71 throw new Error("Non-string groups are not supported by this dummy implementation");72
73 return r[g as keyof SalaryData];74 });75
76 return groupPath.join(" / ");77 });78
79 // Sort the groups to make them nicer80 const rows = Object.entries(groupedData).sort((x, y) => {81 const left = x[0];82 const right = y[0];83
84 const asNumberLeft = Number.parseFloat(left);85 const asNumberRight = Number.parseFloat(right);86
87 if (Number.isNaN(asNumberLeft) || Number.isNaN(asNumberRight)) {88 if (!left && !right) return 0;89 if (!left) return 1;90 if (!right) return -1;91
92 return left.localeCompare(right);93 }94
95 return asNumberLeft - asNumberRight;96 });97
98 return {99 kind: "center",100 asOfTime: Date.now(),101 data: rows.slice(c.start, c.end).map((x) => {102 const childRows = x[1]!;103
104 const nextGroup = groupLevel + 1;105 let childCnt: number;106 if (nextGroup === groupModel.length) childCnt = childRows.length;107 else {108 childCnt = Object.keys(109 Object.groupBy(childRows, (x) => {110 const groupKey = groupModel[nextGroup];111 return x[groupKey as keyof SalaryData];112 }),113 ).length;114 }115
116 return {117 kind: "branch",118 childCount: childCnt,119 data: {}, // See aggregations120 id: x[0],121 key: x[0].split(" / ").at(-1)!,122 };123 }),124
125 path: c.path,126 start: c.start,127 end: c.end,128 size: rows.length,129 } satisfies DataResponse;130 });131}The retry method is invoked in the GroupCellRenderer button component:
1<button2 onClick={() => {3 (grid.state.rowDataSource.get() as RowDataSourceServer<SalaryData>).retry();4 }}5>6 <ExclamationTriangleIcon />7</button>Next Steps
- Server Row Data: Slice and load rows from the server.
- Server Row Grouping: Use the server data source to load and manage group slices.
- Optimistic Loading: Pre-fetch data using optimistic loading to reduce perceived latency.
