AG Grid State Persistence

This document describes the localStorage-based persistence system for AG Grid state across browser sessions and page navigations.

Table of Contents

  1. Overview

  2. Features

  3. Quick Start

  4. Implementation Examples

  5. Logout Integration

  6. Advanced Usage

  7. Storage Format

  8. Performance Notes

  9. Troubleshooting

Overview

The AG Grid state persistence module provides automatic saving and restoration of user grid customizations using localStorage. Each user maintains their own grid state, which persists across browser sessions.

Features

The persistence system supports the following AG Grid features:

  • Column State: Order, width, visibility, pinning

  • Filters: Column filters and advanced filters

  • Sorting: Multi-column sort state

  • Pagination: Current page and page size

  • Row Groups: Group columns and expansion state

  • Cell Selection: Selected cell ranges

  • User-Specific: Separate state per user

  • Auto-Save: Debounced saves on state changes (default 500ms)

  • Row Model Support: Works with both client-side and server-side row models

Quick Start

Step 1: Import the Hook

import { useAgGridState } from '@/hooks/agGrid';
import { useCurrentUser } from '@/auth/hooks/useCurrentUser';

Step 2: Use in Component

const currentUser = useCurrentUser();

const { initialState, onGridReady } = useAgGridState({
  gridId: 'work-orders',
  userId: currentUser.userId || '',
  enabled: !!currentUser.userId,
});

Step 3: Pass to AG Grid

<AgGridReact
  initialState={initialState}
  onGridReady={onGridReady}
  // ... other props
/>

Implementation Examples

Client-Side Grid Example

Complete example for a client-side grid:

import { useCurrentUser } from '@/auth/hooks/useCurrentUser';
import { useAgGridState } from '@/hooks/agGrid';
import { AgGridReact } from 'ag-grid-react';

const WorkOrdersGrid = ({ workOrders, onWorkOrderClick }) => {
  const currentUser = useCurrentUser();

  // Add state persistence
  const { initialState, onGridReady } = useAgGridState({
    gridId: 'work-orders',
    userId: currentUser.userId || '',
    enabled: !!currentUser.userId,
  });

  const gridOptions = useMemo(() => ({
    columnDefs: [
      { field: 'id', headerName: 'ID' },
      { field: 'name', headerName: 'Name' },
      { field: 'status', headerName: 'Status' },
    ],
    defaultColDef: {
      resizable: true,
      sortable: true,
      filter: true,
    },
    pagination: true,
    paginationPageSize: 20,
    rowData: workOrders || [],
  }), [workOrders]);

  return (
    <AgGridReact
      gridOptions={gridOptions}
      initialState={initialState}
      onGridReady={onGridReady}
    />
  );
};

Server-Side Grid Example

Example for a server-side grid with datasource:

import { useCurrentUser } from '@/auth/hooks/useCurrentUser';
import { useAgGridState } from '@/hooks/agGrid';
import { IServerSideDatasource, GridReadyEvent } from 'ag-grid-community';

const AssetGrid = ({ locationId }) => {
  const currentUser = useCurrentUser();
  const gridRef = useRef<AgGridReact>(null);

  // Add state persistence
  const { initialState, onGridReady: onGridReadyState } = useAgGridState({
    gridId: 'assets',
    userId: currentUser.userId || '',
    enabled: !!currentUser.userId,
  });

  // Combine grid ready handlers
  const handleGridReady = useCallback((params: GridReadyEvent) => {
    // Call state persistence handler first
    onGridReadyState(params);

    // Then set up server-side datasource
    const datasource: IServerSideDatasource = {
      getRows: async (serverParams) => {
        try {
          const response = await fetch('/api/assets', {
            method: 'POST',
            body: JSON.stringify(serverParams.request),
          });
          const data = await response.json();

          serverParams.success({
            rowData: data.rows,
            rowCount: data.total,
          });
        } catch (error) {
          serverParams.fail();
        }
      },
    };

    params.api.setGridOption('serverSideDatasource', datasource);
  }, [onGridReadyState]);

  const gridOptions = useMemo(() => ({
    columnDefs: [
      { field: 'name', headerName: 'Asset Name' },
      { field: 'partNumber', headerName: 'Part Number' },
    ],
    rowModelType: 'serverSide',
    serverSideStoreType: 'partial',
  }), []);

  return (
    <AgGridReact
      ref={gridRef}
      gridOptions={gridOptions}
      initialState={initialState}
      onGridReady={handleGridReady}
    />
  );
};

Logout Integration

Clear all saved grid states when a user logs out to prevent state conflicts:

import { clearAllGridStates } from '@/hooks/agGrid';
import { useCurrentUser } from '@/auth/hooks/useCurrentUser';

const handleLogout = async () => {
  const currentUser = useCurrentUser();

  if (currentUser.userId) {
    clearAllGridStates(currentUser.userId);
  }

  // Continue with logout logic
  await msalInstance.logoutRedirect();
};

Advanced Usage

Disable Persistence Conditionally

const { initialState, onGridReady } = useAgGridState({
  gridId: 'work-orders',
  userId: currentUser.userId || '',
  enabled: !!currentUser.userId && !isGuestMode, // Disable for guests
});

Custom Debounce Time

const { initialState, onGridReady } = useAgGridState({
  gridId: 'work-orders',
  userId: currentUser.userId || '',
  debounceMs: 1000, // Wait 1 second before saving
});

Manual State Clearing

const { initialState, onGridReady, clearState } = useAgGridState({
  gridId: 'work-orders',
  userId: currentUser.userId || '',
});

const handleResetGrid = () => {
  clearState();
  window.location.reload(); // Reload to apply cleared state
};

Debug: Check Saved Grids

import { getSavedGridIds } from '@/hooks/agGrid';

const savedGrids = getSavedGridIds(currentUser.userId);
console.log('Grids with saved state:', savedGrids);
// Output: ['work-orders', 'assets', 'locations']

Storage Format

State is stored in localStorage using the following key format:

ems-grid-{gridId}-{userId}

Example storage keys:

  • ems-grid-work-orders-user123

  • ems-grid-assets-user123

  • ems-grid-locations-user456

State Contents

The saved state includes the following (per AG Grid’s GridState interface):

  • version - AG Grid version for migration

  • aggregation - Aggregation functions

  • columnGroup - Opened column groups

  • columnOrder - Column order

  • columnPinning - Pinned columns (left/right)

  • columnSizing - Column widths/flex

  • columnVisibility - Hidden columns

  • filter - Column filters and advanced filter

  • focusedCell - Currently focused cell (client-side only)

  • pagination - Current page

  • pivot - Pivot mode and columns

  • cellSelection - Selected cell ranges

  • rowGroup - Row group columns

  • rowGroupExpansion - Expanded row groups

  • rowSelection - Selected rows (client-side only)

  • sideBar - Side bar state

  • sort - Sort state

Performance Notes

Optimization Strategies

  • Debouncing: State saves are debounced (default 500ms) to prevent excessive localStorage writes

  • Async Operations: All localStorage operations are wrapped in try-catch blocks for error handling

  • Memory Usage: State is stored as JSON strings, typically less than 10KB per grid

  • No Impact: Does not affect grid rendering or data fetching performance

Browser Compatibility

Works in all modern browsers that support:

  • localStorage API

  • ES6+ JavaScript

Troubleshooting

State Not Persisting

  1. Check if user ID is available:

console.log('User ID:', currentUser.userId);
  1. Verify localStorage is enabled:

try {
  localStorage.setItem('test', 'test');
  localStorage.removeItem('test');
  console.log('localStorage is available');
} catch (e) {
  console.error('localStorage is blocked:', e);
}
  1. Check browser console for errors - Look for [AG Grid State] prefixed messages

State Not Restoring

  1. Verify initialState is passed to AgGridReact component

  2. Ensure onGridReady callback is properly connected

  3. Check if state exists in localStorage:

const key = `ems-grid-work-orders-${currentUser.userId}`;
console.log('Saved state:', localStorage.getItem(key));

Migration Notes

AG Grid automatically migrates older state formats when the grid version changes. No manual migration is required.