npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

use-persisting-state-for-react-table

v0.0.1

Published

A helper hook to help persist the state of a Tanstack Table either into the URL or in local storage.

Readme

📊 usePersistingStateForReactTable

A powerful React hook for managing TanStack Table state with automatic persistence across page reloads and browser sessions. Seamlessly persist table configurations including pagination, sorting, filtering, column visibility, and row selection using URL parameters or localStorage with full TypeScript support.

TypeScript MIT License React TanStack Table

✨ Features

  • 🎯 Type-safe - Full TypeScript support with generic types for table data
  • 🔄 Automatic persistence - State changes are persisted instantly to URL or localStorage
  • 📋 Complete table state - Handles pagination, sorting, filters, column visibility, global filter, and row selection
  • 🌐 Flexible storage - Choose URL parameters or localStorage for each state aspect
  • 🚀 Optimistic updates - Support for async filter validation with optimistic update
  • 📦 Custom filter codecs - Define custom serialization for complex filter types
  • 🎨 Filter variants - Built-in support for text, select, date, number, and range filters
  • Performance optimized - Efficient state management with minimal re-renders
  • 🪶 Lightweight - Minimal dependencies focused on React Table integration

📦 Installation

# Using npm
npm install use-persisting-state-for-react-table

# Using yarn
yarn add use-persisting-state-for-react-table

# Using pnpm
pnpm add use-persisting-state-for-react-table

Peer Dependencies

This hook requires the following peer dependencies:

npm install @tanstack/react-table react react-dom

🚀 Quick Start

import { useState } from "react";
import { usePersistingStateForReactTable } from "use-persisting-state-for-react-table";
import {
  useReactTable,
  getCoreRowModel,
  ColumnDef,
} from "@tanstack/react-table";

interface User {
  id: string;
  name: string;
  role: string;
  status: "active" | "inactive";
}

const columns: ColumnDef<User>[] = [
  {
    accessorKey: "name",
    header: "Name",
  },
  {
    accessorKey: "role",
    header: "Role",
    meta: {
      filter: {
        variant: "select",
        persistenceStorage: "url",
        options: [
          { value: "admin", label: "Admin" },
          { value: "user", label: "User" },
          { value: "guest", label: "Guest" },
        ],
      },
    },
  },
  {
    accessorKey: "status",
    header: "Status",
  },
];

function UsersTable() {
  const {
    state,
    handlers,
    resetPagination,
    hasFinishedProcessingAsyncFilters,
  } = usePersistingStateForReactTable({
    columns,
    persistence: {
      urlNamespace: "users-table",
      pagination: {
        pageIndex: { persistenceStorage: "url" },
        pageSize: { persistenceStorage: "url" },
      },
      sorting: { persistenceStorage: "url" },
      globalFilter: { persistenceStorage: "url", key: "search" },
      columnVisibility: { persistenceStorage: "localStorage" },
    },
  });

  const [data, setData] = useState<User[]>([]);

  const table = useReactTable({
    data,
    columns,
    state,
    ...handlers,
    getCoreRowModel: getCoreRowModel(),
    // ... other table configuration
  });

  return <div>{/* Your table UI */}</div>;
}

Your table state will automatically persist across page reloads!

📚 API Reference

usePersistingStateForReactTable(options)

Returns an object with state values, handlers, and utility functions:

  • State object: Contains all current table state (pagination, sorting, columnFilters, columnVisibility, globalFilter, rowSelection)
  • Handlers object: Contains event handlers for React Table integration (onPaginationChange, onSortingChange, onColumnFiltersChange, onColumnVisibilityChange, onGlobalFilterChange, onRowSelectionChange)
  • Utility functions: resetPagination for resetting pagination
  • Async completion indicator: hasFinishedProcessingAsyncFilters boolean for tracking filter validation

Parameters

| Parameter | Type | Description | | --------- | ------------------------------- | ------------------------------------------- | | options | PersistingTableOptions<TData> | Configuration options for table persistence |

Main Options

| Option | Type | Default | Description | | -------------------- | -------------------- | ------- | ---------------------------------------------------------------------- | | columns | ColumnDef<TData>[] | - | Array of column definitions for the table | | automaticPageReset | boolean | true | Automatically resets page index to 0 when filters/global filter change | | initialState | InitialState | {} | Initial state values for table features | | persistence | PersistenceConfig | {} | Configuration for state persistence behavior |

Initial State Configuration

| Option | Type | Default | Description | | ------------------ | --------------------------------------- | ------------------------------ | ------------------------------------ | | columnVisibility | Record<string, boolean> | {} | Initial visibility state for columns | | columnFilters | Array<{id: string, value: any}> | [] | Initial column filter values | | globalFilter | string | "" | Initial global filter value | | rowSelection | Record<string, boolean> | {} | Initial row selection state | | sorting | Array<{id: string, desc: boolean}> | [] | Initial sorting configuration | | pagination | {pageIndex: number, pageSize: number} | {pageIndex: 0, pageSize: 10} | Initial pagination state |

Persistence Configuration

| Option | Type | Default | Description | | ------------------------- | -------------------- | -------------- | ------------------------------------------------------ | | urlNamespace | string | undefined | Namespace prefix for URL parameters to avoid conflicts | | localStorageKey | string | "data-table" | Key for localStorage persistence | | pagination | PaginationConfig | Disabled | Pagination persistence settings | | sorting | SortingConfig | Disabled | Sorting state persistence | | columnVisibility | VisibilityConfig | Disabled | Column visibility persistence | | globalFilter | GlobalFilterConfig | Disabled | Global filter persistence | | rowSelection | RowSelectionConfig | Disabled | Row selection persistence | | filters.optimisticAsync | boolean | false | Enable optimistic updates for async filter validation |

PaginationConfig

| Option | Type | Default | Description | | ------------------------------ | ------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | pageIndex.persistenceStorage | "url" \| "localStorage" | Required | Where to persist the current page index | | pageIndex.key | string | "pageIndex" | Key name for pageIndex persistence | | pageSize.persistenceStorage | "url" \| "localStorage" | Required | Where to persist the page size | | pageSize.key | string | "pageSize" | Key name for pageSize persistence | | pageSize.allowedPageSizes | number[] | undefined | Optional array of allowed page size values. When provided, persisted page sizes are validated against this array. Invalid values fallback to the first allowed value. |

SortingConfig

| Option | Type | Default | Description | | --------------------- | ------------------------- | -------------------- | --------------------------------- | | persistenceStorage | "url" \| "localStorage" | Required | Where to persist sorting state | | sortingColumnKey | string | "sortingColumn" | Key name for the sorted column ID | | sortingDirectionKey | string | "sortingDirection" | Key name for the sort direction |

VisibilityConfig

| Option | Type | Default | Description | | -------------------- | ------------------------- | -------------------- | ---------------------------------------- | | persistenceStorage | "url" \| "localStorage" | Required | Where to persist column visibility state | | key | string | "columnVisibility" | Key name for persistence |

GlobalFilterConfig

| Option | Type | Default | Description | | -------------------- | ------------------------- | ---------------- | ------------------------------------ | | persistenceStorage | "url" \| "localStorage" | Required | Where to persist global filter state | | key | string | "globalFilter" | Key name for persistence |

RowSelectionConfig

| Option | Type | Default | Description | | -------------------- | ------------------------- | ---------------- | ------------------------------------ | | persistenceStorage | "url" \| "localStorage" | Required | Where to persist row selection state | | key | string | "rowSelection" | Key name for persistence |

Return Object

| Property | Type | Description | | ----------------------------------- | --------------------------- | --------------------------------------------------------------------- | | state | TableState | Object containing all current table state values | | state.pagination | PaginationState | Current pagination state | | state.sorting | SortingState | Current sorting state | | state.columnFilters | ColumnFiltersState | Current column filters state | | state.columnVisibility | VisibilityState | Current column visibility state | | state.globalFilter | string | Current global filter state | | state.rowSelection | RowSelectionState | Current row selection state | | handlers | TableHandlers | Object containing handler functions for React Table | | handlers.onPaginationChange | (updater) => void | Handler for pagination changes with automatic persistence | | handlers.onSortingChange | (updater) => void | Handler for sorting changes with automatic persistence | | handlers.onColumnFiltersChange | (updater) => void | Handler for column filter changes with automatic persistence | | handlers.onColumnVisibilityChange | (updater) => void | Handler for column visibility changes with automatic persistence | | handlers.onGlobalFilterChange | (updater: string) => void | Handler for global filter changes with automatic persistence | | handlers.onRowSelectionChange | (updater) => void | Handler for row selection changes with automatic persistence | | resetPagination | () => void | Function to reset pagination to first page while preserving page size | | hasFinishedProcessingAsyncFilters | boolean | Indicates whether async filter validation and cleanup has completed |

🎯 Examples

Basic Usage with URL Persistence

import { usePersistingStateForReactTable } from "use-persisting-state-for-react-table";
import { useReactTable, getCoreRowModel } from "@tanstack/react-table";

function BasicTable() {
  const { state, handlers, hasFinishedProcessingAsyncFilters } =
    usePersistingStateForReactTable({
      columns,
      persistence: {
        urlNamespace: "products",
        pagination: {
          pageIndex: { persistenceStorage: "url", key: "page" },
          pageSize: { persistenceStorage: "url", key: "size" },
        },
        sorting: { persistenceStorage: "url" },
        globalFilter: { persistenceStorage: "url", key: "search" },
      },
    });

  const table = useReactTable({
    data,
    columns,
    state,
    ...handlers,
    getCoreRowModel: getCoreRowModel(),
  });
}

Custom Filter Variants with Persistence

const columns: ColumnDef<Product>[] = [
  {
    accessorKey: "name",
    header: "Product Name",
    meta: {
      filter: {
        variant: "text",
        persistenceStorage: "url",
      },
    },
  },
  {
    accessorKey: "category",
    header: "Category",
    meta: {
      filter: {
        variant: "multiSelect",
        persistenceStorage: "localStorage",
        options: [
          { value: "electronics", label: "Electronics" },
          { value: "clothing", label: "Clothing" },
          { value: "books", label: "Books" },
        ],
      },
    },
  },
  {
    accessorKey: "price",
    header: "Price",
    meta: {
      filter: {
        variant: "numberRange",
        persistenceStorage: "url",
        min: 0,
        max: 1000,
        step: 10,
      },
    },
  },
  {
    accessorKey: "createdAt",
    header: "Created Date",
    meta: {
      filter: {
        variant: "dateRange",
        persistenceStorage: "url",
        fromDate: new Date(2020, 0, 1),
        toDate: new Date(),
      },
    },
  },
];

Custom Serialization for Complex Filters

const columns: ColumnDef<User>[] = [
  {
    accessorKey: "tags",
    header: "Tags",
    meta: {
      filter: {
        variant: "multiSelect",
        persistenceStorage: "url",
        codec: {
          // Custom URL serialization for array of tags
          parse: (str: string) => str.split(",").filter(Boolean),
          format: (tags: string[]) => tags.join(","),
        },
        options: tagOptions,
      },
    },
  },
  {
    accessorKey: "preferences",
    header: "User Preferences",
    meta: {
      filter: {
        variant: "text",
        persistenceStorage: "localStorage",
        codec: {
          // Store complex objects in localStorage
          parse: (str: string) => JSON.parse(str),
          format: (obj: any) => JSON.stringify(obj),
        },
      },
    },
  },
];

Page Size Validation

You can restrict the allowed page sizes to prevent users from setting arbitrary values through URL manipulation or localStorage tampering:

import { usePersistingStateForReactTable } from "use-persisting-state-for-react-table";

function TableWithValidatedPageSize() {
  const { state, handlers, hasFinishedProcessingAsyncFilters } =
    usePersistingStateForReactTable({
      columns,
      persistence: {
        urlNamespace: "products",
        pagination: {
          pageIndex: { persistenceStorage: "url" },
          pageSize: {
            persistenceStorage: "url",
            // Only allow these specific page sizes
            allowedPageSizes: [10, 25, 50, 100],
          },
        },
      },
    });

  const table = useReactTable({
    data,
    columns,
    state,
    ...handlers,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  });

  return (
    <div>
      {/* Your table implementation */}

      {/* Page size selector with only allowed values */}
      <select
        value={state.pagination.pageSize}
        onChange={(e) => table.setPageSize(Number(e.target.value))}
      >
        <option value={10}>10 per page</option>
        <option value={25}>25 per page</option>
        <option value={50}>50 per page</option>
        <option value={100}>100 per page</option>
      </select>
    </div>
  );
}

How it works:

  • When allowedPageSizes is provided, any persisted page size is validated against this array
  • If a user tries to set ?pageSize=15 in the URL but only [10, 25, 50, 100] are allowed, it will automatically fallback to 10 (the first allowed value)
  • This prevents URL manipulation and ensures your UI components (like page size selectors) stay in sync
  • When allowedPageSizes is not provided, no validation occurs (backward compatible behavior)

🔧 Advanced Configuration

Async Filters

The hook provides a way to automatically validate filters values for multi-select or select variants based on values defined on the columns array. Those values can come asynchronously from an API and be validated after the first render.

The hasFinishedProcessingAsyncFilters return value helps you know when all async filter validation and cleanup has completed:

  const { data: filtersFromApi, isLoading: isFiltersLoading } = useQuery({
    queryKey: ["filters"],
    queryFn: () => fetchFilters(),
    enabled: true,
  });

  const columns: ColumnDef<User>[] = useMemo(
    () => [
      ...
      {
        accessorKey: "role",
        id: "Role",
        meta: {
          filter: {
            isLoading: isFiltersLoading,
            variant: "multiSelect",
            options: filtersFromApi?.roles,
            codec: {
              parse: (value) => value.split(","),
              format: (value: string[]) => value.join(",")
            },
            persistenceStorage: "url"
          },
        }
      },
      ...
  , [filtersFromApi, filtersLoading])

  const { state, handlers, hasFinishedProcessingAsyncFilters } = usePersistingStateForReactTable({
    columns,
    ...
  });

If the API gives to us the roles admin, user and manager for example, and when loading the page we have this query param ?role=admin,WRONG-ROLE, this is what's going to happen:

First render

  const { state, handlers } = usePersistingStateForReactTable({
    columns,
    ...
  });

  console.log({ columnFilters: state.columnFilters })
  // {
  //   "columnFilters": []
  // }

After API has finished fetching

console.log({ columnFilters: state.columnFilters });
// {
//   "columnFilters":
//   [
//       { "id": "role", "value": ["admin"] }
//   ]
// }

// query params are now: `?role=admin`

// You can also check if async processing is complete
if (hasFinishedProcessingAsyncFilters) {
  // All filters have been validated and synced
  console.log("Filters are ready for use");
}

With optimisticAsync = true

Enable optimistic updates for filters that trigger async operations to trust the parameter we get first hand

const { state, handlers, hasFinishedProcessingAsyncFilters } =
  usePersistingStateForReactTable({
    columns,
    persistence: {
      filters: {
        optimisticAsync: true, // Enable optimistic updates
      },
      // ... other persistence config
    },
  });

console.log({ columnFilters: state.columnFilters });
// {
//   "columnFilters":
//   [
//       { "id": "role", "value": ["admin", "WRONG-VALUE"] }
//   ]
// }

URL Namespacing

Prevent URL parameter conflicts when using multiple tables:

// Users table
const usersTableState = usePersistingStateForReactTable({
  columns: userColumns,
  persistence: {
    urlNamespace: "users",
    // Results in URL params like: ?users-page=1&users-search=john
  },
});

// Products table
const productsTableState = usePersistingStateForReactTable({
  columns: productColumns,
  persistence: {
    urlNamespace: "products",
    // Results in URL params like: ?products-page=1&products-category=electronics
  },
});

Custom Storage Keys

Customize storage keys for better organization:

const { state, handlers, hasFinishedProcessingAsyncFilters } =
  usePersistingStateForReactTable({
    columns,
    persistence: {
      localStorageKey: "admin-dashboard-table", // Custom localStorage key
      columnVisibility: {
        persistenceStorage: "localStorage",
        key: "column-prefs", // Custom key within localStorage object
      },
      globalFilter: {
        persistenceStorage: "url",
        key: "q", // Short URL parameter for search
      },
      pagination: {
        pageIndex: {
          persistenceStorage: "url",
          key: "p", // Short URL parameter for page
        },
        pageSize: {
          persistenceStorage: "localStorage",
          key: "page-size",
        },
      },
    },
  });

🎨 Filter Variants

The hook supports multiple built-in filter variants with automatic persistence:

Text Filter

meta: {
  filter: {
    variant: "text",
    persistenceStorage: "url",
    key: "search-term",
  },
}

Select Filter

meta: {
  filter: {
    variant: "select",
    persistenceStorage: "url",
    options: [
      { value: "active", label: "Active", count: 42 },
      { value: "inactive", label: "Inactive", count: 8, disabled: true },
    ],
  },
}

Multi-Select Filter

meta: {
  filter: {
    variant: "multiSelect",
    persistenceStorage: "localStorage",
    options: categoryOptions,
  },
}

Date Filter

meta: {
  filter: {
    variant: "date",
    persistenceStorage: "url",
    fromDate: new Date(2020, 0, 1),
    toDate: new Date(),
    captionLayout: "dropdown",
  },
}

Date Range Filter

meta: {
  filter: {
    variant: "dateRange",
    persistenceStorage: "url",
    rangeMinDays: 1,
    rangeMaxDays: 365,
  },
}

Number Filter

meta: {
  filter: {
    variant: "number",
    persistenceStorage: "url",
  },
}

Number Range Filter

meta: {
  filter: {
    variant: "numberRange",
    persistenceStorage: "url",
    min: 0,
    max: 1000,
    step: 50,
    orientation: "horizontal",
  },
}

Getting the meta properties

You can access the meta properties you've defined for each filter in your component that will be using the table helper provided by Tanstack Table:

interface Props<TData> {
  table: Table<TData>;
}

export function Component<TData>({
  table,
}: Props<TData>) {
  const columns = table.getAllColumns();

  return (
    <div>
      {columns.filter((col) => col.getCanFilter())
        .map((column) => {
          const meta = column.columnDef.meta
          return <div>{meta.filter.key} is of type {meta.filter.variant}</div>
      })
    </div>
  )
}

📝 TypeScript Support

The hook provides full TypeScript support with generic types:

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user" | "guest";
  isActive: boolean;
  createdAt: Date;
}

// Full type safety for table data
const { state, handlers, hasFinishedProcessingAsyncFilters } =
  usePersistingStateForReactTable<User>({
    columns: userColumns,
    initialState: {
      sorting: [{ id: "name", desc: false }], // ✅ Valid column ID
      columnFilters: [{ id: "role", value: "admin" }], // ✅ Valid
      pagination: { pageIndex: 0, pageSize: 25 }, // ✅ Valid
    },
    persistence: {
      columnVisibility: { persistenceStorage: "localStorage" }, // ✅ Valid
    },
  });

// TypeScript will catch errors
const invalidConfig = usePersistingStateForReactTable<User>({
  columns: userColumns,
  initialState: {
    sorting: [{ id: "invalidColumn", desc: false }], // ❌ TypeScript error
    pagination: { pageIndex: "invalid", pageSize: 25 }, // ❌ TypeScript error
  },
});

Extending ColumnMeta

This package extends TanStack Table's ColumnMeta interface to add filter metadata. If you need to add your own custom properties to ColumnMeta, you can use the provided ExtendColumnMeta utility type:

import { ExtendColumnMeta } from "use-persisting-state-for-react-table";
import "@tanstack/react-table";

// Define your custom meta properties
type MyColumnMeta = ExtendColumnMeta<{
  newProp: string;
}>;

// Extend the ColumnMeta interface
declare module "@tanstack/react-table" {
  interface ColumnMeta<TData extends RowData, TValue> extends MyColumnMeta {}
}

// Now you can use both filter properties and your custom properties
const columns: ColumnDef<User>[] = [
  {
    accessorKey: "name",
    header: "Name",
    meta: {
      // Filter metadata from this package
      filter: {
        variant: "text",
        persistenceStorage: "url",
      },
      // Your custom properties
      newProp: "Custom value",
    },
  },
];

This approach ensures that:

  • ✅ You get full TypeScript support for both filter metadata and your custom properties
  • ✅ The filter functionality from this package continues to work
  • ✅ Your custom properties are type-safe and available in IntelliSense
  • ✅ No conflicts occur between different extensions

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Setup

# Clone the repository
git clone https://github.com/lucasriondel/use-persisting-state-for-react-table.git

# Install dependencies
pnpm install

# Run tests
pnpm test

# Build the package
pnpm build

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments


Happy coding! 🚀 If you find this hook useful, please consider giving it a ⭐ on GitHub!