Troubleshooting
The grid is not sorting or filtering
Check dataMode. The default is 'server', which means the grid passes state to onStateChange but does not sort or filter rows itself. If you pass all your data and expect the grid to handle it, set dataMode="client".
// Wrong — dataMode defaults to 'server', so the grid does nothing with the data
<DataGrid data={allRows} ... />
// Correct
<DataGrid data={allRows} dataMode="client" ... />onStateChange fires on every render
You're likely passing an inline function that gets recreated on every render, but because the grid wraps the callback in a ref internally, onStateChange only fires when the query state (sorts, filters, grouping, pagination) actually changes. If you're seeing double-fires, check whether your server fetch function is triggering a state update that causes a re-render with different state.
Use useCallback to keep the function reference stable and avoid subtle dependency issues:
const handleStateChange = useCallback(async (state: GridState) => {
// fetch data
}, []) // add any external dependencies hereSelect filter dropdown is empty
Three possible causes:
filterType: 'select'is not set on the column. Without it, the column usestextfiltering.- Server mode with no
fetchFilterOptions. IndataMode="server", the grid can't derive options from the full dataset. PassfetchFilterOptions:tsxfetchFilterOptions={async (colId) => { const res = await fetch(`/api/options?column=${colId}`) return res.json() }} accessorreturns values that are not strings. Auto-derivation in client mode callsString(value)on the accessor result. Ifaccessorreturnsnullorundefined, those values are skipped.
Row count shows 0 in server mode
Set rowCount to the total number of matching rows from your API response. Without it, the pagination control has no way to know how many pages there are.
<DataGrid
dataMode="server"
data={currentPageRows}
rowCount={totalFromApi} // ← required
...
/>initialState changes are ignored after mount
initialState is read once during the initial render and ignored on subsequent renders — it's not reactive. To change state after mount, use the imperative ref:
ref.current?.setState({ filters: [...] })Column widths are out of sync with virtual scrolling
Both tables in the virtual-scroll layout share a <colgroup> element. If you set an explicit width on a column, make sure the value is a number (pixels), not a percentage — percentage widths are calculated differently in detached <col> elements.
// Good
{ id: 'name', width: 200 }
// May cause misalignment
{ id: 'name', width: '20%' as any }Sort or filter state is reset when I navigate away and back
State lives inside the component's useReducer. Unmounting and remounting the component resets it to initialState. To persist state across navigation:
- Encode it in the URL (see the URL sync recipe)
- Lift state to a parent component and pass it as
initialStateeach time
Portaled dropdowns (number filter, column menu) appear at (0, 0)
This happens in jsdom tests because getBoundingClientRect() returns all zeroes. The dropdowns will appear at top: 0, left: 0 in the rendered output. This is not a bug — it's a jsdom limitation.
If you're testing filter interactions, assert on the input values and filter state rather than dropdown position:
// Don't assert position
// expect(dropdown).toHaveStyle({ top: '200px' })
// Assert on behaviour
userEvent.click(filterIcon)
userEvent.type(input, '100')
expect(screen.getByDisplayValue('100')).toBeInTheDocument()Tests fail with role="gridcell" not found
jsdom does not implement the full ARIA role hierarchy, so role="gridcell" is not recognised. Use the raw DOM selector instead:
// Fails in jsdom
screen.getAllByRole('gridcell')
// Works
container.querySelectorAll('tbody td')Similarly, interactive rows with role="button" are missed by getAllByRole('row'):
// May miss interactive rows
screen.getAllByRole('row')
// Always works
container.querySelectorAll('tbody tr')TypeScript: Type 'string' is not assignable to type 'FilterOperator'
The operator field in FilterEntry is a string union, not a plain string. Use as const or cast explicitly when constructing filter objects outside the grid:
// Error
const filter = { columnId: 'status', operator: 'eq', value: 'active' }
// Fix
const filter = { columnId: 'status', operator: 'eq' as const, value: 'active' }AI bar does nothing / shows an error
- Missing API key — check that
ANTHROPIC_API_KEY(orOPENAI_API_KEY) is set in your environment. For local dev, put it inexample/.env.local(not the project root.env). - Wrong endpoint — the
ai.endpointprop must match the URL your server handles. For the Vite dev server, use'/api/grid-ai'exactly. - Not running
dev:example— the AI middleware is registered invite.config.tsand only runs when you start withnpm run dev:example. Runningnpm run devfrom theexample/directory won't load the middleware. - CORS — if your grid is on a different origin from the AI endpoint, add appropriate CORS headers on the server.
Build error: Cannot find module 'yk-grid/server/gridAiRoute'
The server/ directory is not bundled. It's intended for use directly in Node.js environments (Express, Next.js). If you're trying to import it in a Vite or webpack frontend bundle, that's the issue.
Import it only from server-side code:
// ✓ Server file (Node.js)
import { handleGridAiRequest } from 'yk-grid/server/gridAiRoute'
// ✗ Client component — will fail
import { handleGridAiRequest } from 'yk-grid/server/gridAiRoute'If your tsconfig or bundler follows the types field in package.json, add a path alias in your tsconfig to resolve the server path explicitly.