LyteNyte Grid is built on declarative principles. Its state system is fully reactive, automatically keeping the view synchronized with state changes. This follows React's core philosophy that "view is a function of state."
This reactivity can be leveraged naturally in your React code, integrating smoothly with other components. LyteNyte Grid achieves this by using standard React primitives, enabling seamless interaction throughout your application.
The grid state object serves as your interface to LyteNyte Grid. It's returned by the useLyteNytePro
(or useLyteNyteCore
)
hook and contains a state
property. Examining this object's types reveals that most keys match
those in the initial state object passed to useLyteNytePro
- this is intentional. LyteNyte Grid also
adds several helpful fields.
While the keys match, the values differ significantly. Values in the state object are signals (observable values) specifically designed for LyteNyte Grid's needs.
Each signal provides methods including watch
, peek
, set
, and use
. To integrate a signal with
React, call the use
method - this React hook retrieves the current state value and triggers
re-renders when that value changes. The set
method updates a signal's value.
This becomes clearer with an example:
"use client";
import {
LyteNyteGrid,
useLyteNytePro,
useClientDataSource,
} from "@1771technologies/lytenyte-pro";
import "@1771technologies/lytenyte-pro/grid.css";
import { ColumnProReact } from "@1771technologies/lytenyte-pro/types";
import { bankDataSmall } from "@1771technologies/sample-data/bank-data-smaller";
import { useMemo, useId } from "react";
const columns: ColumnProReact[] = [
{ id: "age", type: "number" },
{ id: "job" },
{ id: "balance", type: "number" },
{ id: "education" },
{ id: "marital" },
{ id: "default" },
{ id: "housing" },
{ id: "loan" },
{ id: "contact" },
{ id: "day", type: "number" },
{ id: "month" },
{ id: "duration" },
{ id: "campaign" },
{ id: "pdays" },
{ id: "previous" },
{ id: "poutcome" },
{ id: "y" },
];
export function GridReactivitySignal() {
const ds = useClientDataSource({ data: bankDataSmall });
const grid = useLyteNytePro({
gridId: useId(),
rowDataSource: ds,
columns,
rowSelectionSelectedIds: new Set(["3-center", "4-center"]),
rowSelectionCheckbox: "normal",
rowSelectionMode: "multiple",
rowSelectionMultiSelectOnClick: true,
rowSelectionPointerActivator: "single-click",
});
const selectedRows = grid.state.rowSelectionSelectedIds.use();
const selectedRow = useMemo(() => {
if (!selectedRows.size) return null;
return [...selectedRows.values()].join(", ");
}, [selectedRows]);
return (
<div style={{ height: 700, display: "flex", flexDirection: "column" }}>
<div>
<button
onClick={() => grid.state.rowSelectionSelectedIds.set(new Set())}
>
Clear Row Selection
</button>
</div>
<div style={{ padding: 8, display: "flex", gap: 8 }}>
{selectedRow && `Currently selected rows: ${selectedRow}`}
{!selectedRow && "No rows selected"}
</div>
<div style={{ flex: "1" }}>
<LyteNyteGrid grid={grid} />
</div>
</div>
);
}
The power of the use
call extends beyond basic reactivity. Since LyteNyte Grid's state lives in the
object returned by useLyteNytePro
, you can elevate this state to higher component levels and share it
throughout your application.
For instance, you can build a table that presents a line chart based on the selected row. We do this by
passing the grid state to a price chart component. In the price chart component we use grid.state.rowSelectionSelectedIds.use
function to reactively subscribe to changes to the row selection:
"use client";
import {
LyteNyteGrid,
useLyteNytePro,
useClientDataSource,
} from "@1771technologies/lytenyte-pro";
import "@1771technologies/lytenyte-pro/grid.css";
import {
ColumnProReact,
GridProReact,
} from "@1771technologies/lytenyte-pro/types";
import { companiesWithPricePerf } from "@1771technologies/sample-data/companies-with-price-performance";
import { useMemo, useId } from "react";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis,
} from "recharts";
type RowData = (typeof companiesWithPricePerf)[number];
const columns: ColumnProReact[] = [
{ id: "Company" },
{ id: "Founded" },
{ id: "Employee Cnt" },
{ id: "Country" },
{ id: "Price" },
];
export function GridReactivityChart() {
const ds = useClientDataSource({ data: companiesWithPricePerf });
const grid = useLyteNytePro({
gridId: useId(),
rowDataSource: ds,
columns,
rowSelectionSelectedIds: new Set(["0-center"]),
rowSelectionCheckbox: "normal",
rowSelectionMode: "multiple",
rowSelectionMultiSelectOnClick: true,
rowSelectionPointerActivator: "single-click",
});
return (
<div style={{ height: 800, display: "flex", flexDirection: "column" }}>
<div style={{ flex: "1" }}>
<LyteNyteGrid grid={grid} />
</div>
<div style={{ padding: "10px 0px", borderTop: "1px solid gray" }}>
{" "}
<PriceChart grid={grid} />
</div>
</div>
);
}
function PriceChart({ grid }: { grid: GridProReact<RowData> }) {
const state = grid.state;
const selected = state.rowSelectionSelectedIds.use();
const rows = useMemo(() => {
return [...selected.values()]
.map((rowId) => grid.api.rowById(rowId))
.filter((row) => !!row)
.sort((l, r) => l.id.localeCompare(r.id));
}, [grid.api, selected]);
const data = useMemo(() => {
const weeks: Record<string, { week: number; [key: string]: number }> =
Object.fromEntries(
Array.from({ length: 52 }, (_, i) => [i + 1, { week: i + 1 }])
);
rows.forEach((row) => {
if (!row || !grid.api.rowIsLeaf(row)) return;
const data = row.data["1 Year Perf"];
data.forEach((dp, i) => {
weeks[i + 1][row.id] = dp;
});
});
return Object.values(weeks).sort((l, r) => l.week - r.week);
}, [grid.api, rows]);
return (
<ResponsiveContainer height={300} width="100%">
<AreaChart data={data}>
<defs>
{rows.map((row, i) => {
const color = colors[i];
return (
<linearGradient
key={row.id}
id={row.id}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor={color.stop5} stopOpacity={0.8} />
<stop offset="95%" stopColor={color.stop95} stopOpacity={0} />
</linearGradient>
);
})}
</defs>
<XAxis dataKey="week" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
{rows.map((row, i) => {
const color = colors[i];
return (
<Area
key={row.id}
type="monotone"
dataKey={row.id}
stroke={color.solid}
fillOpacity={1}
fill={`url(#${row.id})`}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
);
}
const colors = [
{ name: "Ruby Red", solid: "#E02D3F", stop5: "#FBE6E8", stop95: "#E33E4F" },
{ name: "Coral", solid: "#FF7F50", stop5: "#FFEFE9", stop95: "#FF8C61" },
{ name: "Amber", solid: "#FFBF00", stop5: "#FFF7E0", stop95: "#FFC519" },
{
name: "Golden Yellow",
solid: "#FFD700",
stop5: "#FFFAE0",
stop95: "#FFDB19",
},
{ name: "Lime Green", solid: "#32CD32", stop5: "#E7F9E7", stop95: "#47D247" },
{ name: "Emerald", solid: "#50C878", stop5: "#EAF8EF", stop95: "#62CE86" },
{ name: "Teal", solid: "#008080", stop5: "#E0F0F0", stop95: "#199999" },
{ name: "Sky Blue", solid: "#87CEEB", stop5: "#F1F9FD", stop95: "#93D4ED" },
{ name: "Royal Blue", solid: "#4169E1", stop5: "#E8EDFC", stop95: "#5479E4" },
{ name: "Indigo", solid: "#4B0082", stop5: "#E9E0F0", stop95: "#5C1993" },
{ name: "Purple", solid: "#800080", stop5: "#F0E0F0", stop95: "#911991" },
{ name: "Magenta", solid: "#FF00FF", stop5: "#FFE0FF", stop95: "#FF19FF" },
{ name: "Hot Pink", solid: "#FF69B4", stop5: "#FFEDF6", stop95: "#FF78BB" },
{ name: "Chocolate", solid: "#D2691E", stop5: "#F9EEE4", stop95: "#D77935" },
{ name: "Sienna", solid: "#A0522D", stop5: "#F3EAE6", stop95: "#AC6542" },
{ name: "Olive", solid: "#808000", stop5: "#F0F0E0", stop95: "#919119" },
{
name: "Forest Green",
solid: "#228B22",
stop5: "#E5F1E5",
stop95: "#399939",
},
{ name: "Navy Blue", solid: "#000080", stop5: "#E0E0F0", stop95: "#191999" },
{ name: "Slate Gray", solid: "#708090", stop5: "#EEF0F2", stop95: "#7F8C9A" },
{ name: "Charcoal", solid: "#36454F", stop5: "#E7E9EA", stop95: "#4B5962" },
{ name: "Crimson", solid: "#DC143C", stop5: "#FAE3E7", stop95: "#E02A4F" },
{ name: "Turquoise", solid: "#40E0D0", stop5: "#E8FBF9", stop95: "#53E3D4" },
{ name: "Violet", solid: "#8A2BE2", stop5: "#F1E6FB", stop95: "#9641E5" },
{ name: "Salmon", solid: "#FA8072", stop5: "#FEEFED", stop95: "#FB8D80" },
{ name: "Tan", solid: "#D2B48C", stop5: "#F9F6F1", stop95: "#D7BD98" },
{ name: "Maroon", solid: "#800000", stop5: "#F0E0E0", stop95: "#991919" },
{ name: "Aquamarine", solid: "#7FFFD4", stop5: "#EFFFF9", stop95: "#8CFFD8" },
{ name: "Steel Blue", solid: "#4682B4", stop5: "#E9EFF6", stop95: "#588FBB" },
{ name: "Khaki", solid: "#C3B091", stop5: "#F7F5F1", stop95: "#C9B99D" },
{ name: "Plum", solid: "#8E4585", stop5: "#F1E9F0", stop95: "#9B5892" },
{ name: "Navy Blue", solid: "#000080", stop5: "#E0E0F0", stop95: "#191999" },
{ name: "Slate Gray", solid: "#708090", stop5: "#EEF0F2", stop95: "#7F8C9A" },
];
This approach lets you maintain React's recommended one-way data flow without resorting to complex synchronization methods.
Beyond use
, signal objects offer several other methods for specific scenarios.
peek
MethodThe peek
method retrieves a signal's current value without following hook rules, making it callable
from anywhere in your code.
You may notice a get
method on signal objects. While functionally identical to peek
, get
is
used internally by LyteNyte Grid's state library for signal connections. Though safe to use,
prefer peek
in your own code.
set
MethodThe set
method changes a signal's value. Similar to React's setState
, it accepts either a new
value or a function that receives the current value and returns an updated one:
const grid = useLyteNytePro({});
grid.state.rowHeight.set(22);
// Or
grid.state.rowHeight.set((prev) => prev + 10); // current height + 10
Important: Like React's useState
setter, there's no guarantee a newly set value is immediately
available after calling set
. Avoid code that immediately accesses a newly set value:
grid.state.rowHeight.set(24);
// INCORRECT - may return 24 or the previous value
const row = grid.state.rowHeight.peek();
Instead, capture the current value first, then set the new value:
const currentRowHeight = grid.state.rowHeight.peek();
grid.state.rowHeight.set(24);
watch
MethodThe watch
method monitors changes to a signal's value. It shouldn't be used in React's render path
(place it inside useEffect
or event handlers instead). Think of watch
as creating an event
listener for value changes, requiring proper cleanup when used.
For example, to log each row height change:
const grid = useLyteNytePro({});
const rowHeightSignal = grid.state.rowHeight;
useEffect(() => {
const remove = rowHeightSignal.watch(() => {
console.log(rowHeightSignal.peek());
});
return () => remove();
}, [rowHeightSignal]);
Note that we read the signal's value inside the watch callback. The callback doesn't receive the
changes; it simply notifies when changes occur. This happens because LyteNyte Grid evaluates state
changes lazily - multiple consecutive changes to the same signal trigger only a single watch
call:
rowHeightSignal.set(24);
rowHeightSignal.set(32);
rowHeightSignal.set(18);
The code above results in just one watch
call. Additionally, watch
callbacks execute on the
microtask queue.