233 lines
8.6 KiB
Markdown
233 lines
8.6 KiB
Markdown
# ⚡ react-virtual-aggridautomat
|
|
|
|
> Opinionated, fully-virtualized AG Grid component powered by [react-automat](https://github.com/sebseb7/react-automat) — infinite-scroll data grids as React `PureComponent` with zero hooks, HOCs, or Context Providers.
|
|
|
|
[](https://www.ag-grid.com/)
|
|
[](https://github.com/sebseb7/react-automat)
|
|
[](https://react.dev/)
|
|
|
|
---
|
|
|
|
## What is this?
|
|
|
|
A reusable `<AutomatGrid />` PureComponent that wires AG Grid's [Infinite Row Model](https://www.ag-grid.com/react-data-grid/infinite-scrolling/) to a `react-automat` store. Your async data fetcher is the only thing you provide — the library handles:
|
|
|
|
- **Virtual row loading** — only visible rows exist in the DOM
|
|
- **Block caching** — configurable cache size, debounced scroll loading
|
|
- **Server-side sort & filter** — sort/filter changes flow through the automat and trigger datasource resets
|
|
- **Reactive state** — loading indicators, error state, row counts, and revision tracking all live in the automat
|
|
- **Zero hooks** — designed for `PureComponent` with synchronous constructor reads and fine-grained automat subscriptions
|
|
|
|
## Install
|
|
|
|
```bash
|
|
npm install ag-grid-community ag-grid-react react-automat
|
|
```
|
|
|
|
> This library is not yet published to npm. Clone or copy `src/lib/` into your project.
|
|
|
|
## Quick Start
|
|
|
|
### 1. Create a grid automat
|
|
|
|
```ts
|
|
// myGridAutomat.ts
|
|
import { createGridAutomat } from './lib';
|
|
|
|
export const myGridAutomat = createGridAutomat({
|
|
fetchRows: async ({ startRow, endRow, sortModel, filterModel }) => {
|
|
const res = await fetch(`/api/items?start=${startRow}&end=${endRow}`);
|
|
const { items, total } = await res.json();
|
|
return { rows: items, lastRow: total };
|
|
},
|
|
name: 'myGrid', // Visible in window.__AUTOMATS__
|
|
blockSize: 100, // Rows per block (passed to AG Grid's cacheBlockSize)
|
|
});
|
|
```
|
|
|
|
### 2. Render the grid
|
|
|
|
```tsx
|
|
// MyView.tsx
|
|
import { PureComponent } from 'react';
|
|
import { AutomatGrid } from './lib';
|
|
import { myGridAutomat } from './myGridAutomat';
|
|
|
|
class MyView extends PureComponent {
|
|
render() {
|
|
return (
|
|
<AutomatGrid
|
|
automat={myGridAutomat}
|
|
columnDefs={[
|
|
{ field: 'id', maxWidth: 80 },
|
|
{ field: 'name', flex: 2, filter: 'agTextColumnFilter' },
|
|
{ field: 'price', valueFormatter: p => `€${p.value?.toFixed(2)}` },
|
|
]}
|
|
rowIdField="id"
|
|
height="600px"
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. React to grid state
|
|
|
|
The automat is a standard `react-automat` store — subscribe to it from any PureComponent:
|
|
|
|
```tsx
|
|
class StatusBar extends PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
loading: myGridAutomat.state.loading,
|
|
totalRows: myGridAutomat.state.totalRows,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.unsub = myGridAutomat.subscribe(this, (s) => ({
|
|
loading: s.loading,
|
|
totalRows: s.totalRows,
|
|
}));
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.unsub();
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div>
|
|
{this.state.loading ? 'Loading…' : `${this.state.totalRows} rows`}
|
|
<button onClick={() => myGridAutomat.actions.reload()}>↻ Reload</button>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### `createGridAutomat(options)`
|
|
|
|
Creates a `react-automat` store to back the grid.
|
|
|
|
| Option | Type | Default | Description |
|
|
|---|---|---|---|
|
|
| `fetchRows` | `(params: GridFetchParams) => Promise<GridFetchResult>` | **required** | Async function called by AG Grid's datasource for each block |
|
|
| `name` | `string` | `null` | Automat registry name (visible in `window.__AUTOMATS__`) |
|
|
| `blockSize` | `number` | `100` | Rows per cache block |
|
|
|
|
#### `GridFetchParams`
|
|
|
|
```ts
|
|
{
|
|
startRow: number;
|
|
endRow: number;
|
|
sortModel: SortModelItem[];
|
|
filterModel: Record<string, unknown>;
|
|
}
|
|
```
|
|
|
|
#### `GridFetchResult<TRow>`
|
|
|
|
```ts
|
|
{
|
|
rows: TRow[];
|
|
lastRow?: number; // Total count. undefined/-1 = unknown (infinite scroll continues)
|
|
}
|
|
```
|
|
|
|
#### Automat State
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `ready` | `boolean` | `true` after first block loads |
|
|
| `loading` | `boolean` | `true` while any block is in-flight |
|
|
| `totalRows` | `number` | Known row count (`-1` if unknown) |
|
|
| `sortModel` | `SortModelItem[]` | Current sort state |
|
|
| `filterModel` | `Record<string, unknown>` | Current filter state |
|
|
| `revision` | `number` | Increments on reload/sort/filter change |
|
|
| `error` | `string \| null` | Last fetch error message |
|
|
|
|
#### Automat Actions
|
|
|
|
| Action | Description |
|
|
|---|---|
|
|
| `reload()` | Bumps revision, clears cache, forces full refetch |
|
|
| `setSort(sortModel)` | Updates sort model + reloads |
|
|
| `setFilter(filterModel)` | Updates filter model + reloads |
|
|
|
|
### `<AutomatGrid />` Props
|
|
|
|
| Prop | Type | Default | Description |
|
|
|---|---|---|---|
|
|
| `automat` | `Automat<GridAutomatState, GridAutomatActions>` | **required** | The grid automat store |
|
|
| `columnDefs` | `ColDef[]` | **required** | AG Grid column definitions |
|
|
| `defaultColDef` | `ColDef` | `{ flex: 1, minWidth: 100, sortable: true, resizable: true }` | Default column config |
|
|
| `rowIdField` | `string` | `'id'` | Row data field used for stable row IDs |
|
|
| `cacheBlockSize` | `number` | automat's `blockSize` | Override cache block size |
|
|
| `rowBuffer` | `number` | `0` | Extra rows rendered outside viewport |
|
|
| `infiniteInitialRowCount` | `number` | `200` | Scrollbar size hint |
|
|
| `maxConcurrentDatasourceRequests` | `number` | `2` | Parallel block fetches |
|
|
| `maxBlocksInCache` | `number` | `10` | Max cached blocks before eviction |
|
|
| `blockLoadDebounceMillis` | `number` | `100` | Scroll debounce |
|
|
| `height` | `string \| number` | `'100%'` | Container height |
|
|
| `width` | `string \| number` | `'100%'` | Container width |
|
|
| `rowHeight` | `number` | AG Grid default (42) | Row height in pixels |
|
|
| `theme` | `string` | `'ag-theme-alpine ag-theme-alpine-dark'` | Container theme class |
|
|
| `modules` | `Module[]` | InfiniteRowModel + Text/Number/Date/Custom filters | Additional AG Grid modules |
|
|
| `onGridApiReady` | `(api: GridApi) => void` | — | Callback when AG Grid API is available |
|
|
| `className` | `string` | — | Container CSS class |
|
|
| `style` | `CSSProperties` | — | Container inline styles |
|
|
|
|
## Data Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ AutomatGrid (PureComponent) │
|
|
│ │
|
|
│ constructor: reads automat.state synchronously │
|
|
│ componentDidMount: subscribes with selector │
|
|
│ componentDidUpdate: if revision changed → reset ds │
|
|
│ │
|
|
│ ┌──────────────┐ ┌────────────────────────┐ │
|
|
│ │ AG Grid │────▶│ IDatasource.getRows() │ │
|
|
│ │ (infinite │ │ calls automat._fetcher │ │
|
|
│ │ row model) │◀────│ → successCallback() │ │
|
|
│ └──────────────┘ └────────────────────────┘ │
|
|
│ │ │ │
|
|
│ onSortChanged ──────▶ automat.actions.setSort() │
|
|
│ onFilterChanged ────▶ automat.actions.setFilter() │
|
|
│ │ │ │
|
|
│ └──── revision bump ─────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Running the Demo
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
The demo generates 10,000 fake product rows and serves them through a simulated async fetcher with artificial latency. Open http://localhost:5173 to see infinite scroll in action.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
src/
|
|
├── lib/ # Reusable library
|
|
│ ├── createGridAutomat.ts # Automat factory
|
|
│ ├── AutomatGrid.tsx # PureComponent grid wrapper
|
|
│ └── index.ts # Barrel exports
|
|
└── demo/ # Demo app
|
|
├── demoAutomat.ts # 10k fake rows + fetcher
|
|
└── DemoGrid.tsx # Demo consumer component
|
|
```
|
|
|
|
## License
|
|
|
|
0BSD
|