gridcn1.2.2

Server side

The full remote contract. Sort, filter, search, and pagination sent to the server in one query per view state.

Everything in one place: header-click sort, the filter menu, quick search, and server pagination all land in a single request. The view state is the query key, so revisiting a state is instant from the cache, and keepPreviousData keeps the previous page on screen while the next one loads. Nothing is filtered or sorted client-side. The fake server applies every operator.

Loading…sort, filter, search and page are all server-side
Name
Email
Role
Score
No rows
1–25 of 1000
pnpm dlx shadcn@latest add @gridcn/data-grid-server-side-demo

The contract

The grid's store lives inside the provider, but the provider's data prop sits outside it. So a small bridge component does both jobs: it reads the store (it has to be inside the provider) and reports the fetched page up (the provider needs it).

function RemoteStateBridge({ server, page, pageSize, onLoaded }) {
  const sortState = useDataGridSortState();
  const filterState = useDataGridFilterState();
  const joinOperator = useDataGridJoinOperator();
  const searchText = useDataGridSearchText();

  const query = useQuery({
    queryKey: ["rows", page, pageSize, sortState, filterState, joinOperator, searchText],
    queryFn: () =>
      server.fetchPage({
        page,
        pageSize,
        sorts: sortState,
        filters: filterState,
        join: joinOperator,
        search: searchText,
      }),
    placeholderData: keepPreviousData,
  });

  useEffect(() => {
    if (query.data) onLoaded(query.data);
  }, [query.data, onLoaded]);

  return null;
}

Notes:

  • The store hooks return stable references while the state is unchanged (useShallow), so the key only changes when a user actually sorts, filters, or searches (no render-time refetch loop).

  • Pagination uses the clamped page from pager.controls: when a filter shrinks the result set, the bridge fetches the clamped page, never one past the end.

  • Edits write through and invalidate, same as the pagination pages:

    const onDataChange = (_next: readonly Row[], change: DataChange<Row>) => {
      api.save(change.ops);
      void queryClient.invalidateQueries({ queryKey: ["rows"] });
    };

SWR variant

Same contract, different cache. The delta is the key/queryFn pair. The key becomes the whole serialized view state, and the fetcher reads it positionally:

const query = useQuery({
  queryKey: ["rows", page, pageSize, sortState, filterState, joinOperator, searchText],
  queryFn: () =>
    api.fetchPage({
      page,
      pageSize,
      sorts: sortState,
      filters: filterState,
      join: joinOperator,
      search: searchText,
    }),
  placeholderData: keepPreviousData,
});

With SWR the previous-page-while-loading behavior has to be kept by hand (last data in a ref, or the local-state pattern from the pagination SWR page).

Shareable state

Wrap the same fetch in DataGridUrlState and the view state (sort, filters, search, page, page size) moves to the URL: reload or share the link and the grid rebuilds the exact same request. The bridge's query key then reads the URL-synced state instead of the store, and the URL becomes the request itself.

Behavior reference

On this page