useQueryParamsState

State Management

A React hook that manages state in URL query parameters, perfect for shareable and bookmarkable UI states.

Demo

These filters are stored in URL query parameters and can be shared or bookmarked:

Installation

npm install @thibault.sh/hooks

Usage

import { useQueryParamsState } from '@thibault.sh/hooks/useQueryParamsState';

function SearchPage() {
  const [searchParams, setSearchParams] = useQueryParamsState('q', {
    query: '',
    page: 1,
    limit: 10
  });
  
  return (
    <div>
      <input
        value={searchParams.query}
        onChange={(e) => setSearchParams(prev => ({
          ...prev,
          query: e.target.value,
          page: 1 // Reset page when query changes
        }))}
        placeholder="Search..."
      />
      <div>
        Page {searchParams.page} of results
        <button onClick={() => setSearchParams(prev => ({
          ...prev,
          page: prev.page + 1
        }))}>
          Next Page
        </button>
      </div>
    </div>
  );
}

API

Parameters

  • key
    string

    The query parameter key

  • initialValue
    T

    The initial value to use if the parameter doesn't exist

  • options
    Object

    Configuration options

  • options.serialize
    (value: T) => string

    Function to convert value to string (default: JSON.stringify)

  • options.deserialize
    (value: string) => T

    Function to parse string back to value (default: JSON.parse)

Returns

A tuple containing the current value and a setter function

Custom Serialization

// Example with custom serialization for dates
const [dateRange, setDateRange] = useQueryParamsState(
  'range',
  {
    start: new Date(),
    end: new Date()
  },
  {
    serialize: (value) => ({
      start: value.start.toISOString(),
      end: value.end.toISOString()
    }),
    deserialize: (value) => ({
      start: new Date(value.start),
      end: new Date(value.end)
    })
  }
);

Features

  • URL Persistence

    State is stored in the URL, making it shareable and bookmarkable

  • Custom Serialization

    Support for custom serialization and deserialization of complex data types

  • Type Safety

    Full TypeScript support with generics

  • Browser History Integration

    Works seamlessly with browser history and navigation

Table View Example

import { useQueryParamsState } from '@thibault.sh/hooks';

interface TableState {
  page: number;
  pageSize: number;
  sortColumn: string;
  sortDirection: 'asc' | 'desc';
  filters: Record<string, string>;
}

function DataTable() {
  const [tableState, setTableState] = useQueryParamsState<TableState>(
    'table',
    {
      page: 1,
      pageSize: 10,
      sortColumn: 'id',
      sortDirection: 'asc',
      filters: {}
    }
  );

  const handleSort = (column: string) => {
    setTableState(prev => ({
      ...prev,
      sortColumn: column,
      sortDirection: prev.sortColumn === column && prev.sortDirection === 'asc'
        ? 'desc'
        : 'asc'
    }));
  };

  const handleFilter = (column: string, value: string) => {
    setTableState(prev => ({
      ...prev,
      page: 1, // Reset to first page when filtering
      filters: {
        ...prev.filters,
        [column]: value
      }
    }));
  };

  return (
    <div>
      {/* Table implementation */}
    </div>
  );
}

Map View Example

import { useQueryParamsState } from '@thibault.sh/hooks';

interface MapViewState {
  center: {
    lat: number;
    lng: number;
  };
  zoom: number;
  layers: string[];
}

function MapView() {
  const [mapState, setMapState] = useQueryParamsState<MapViewState>(
    'map',
    {
      center: { lat: 0, lng: 0 },
      zoom: 2,
      layers: ['terrain']
    },
    {
      // Custom serialization to make URLs cleaner
      serialize: (value) => ({
        c: `${value.center.lat},${value.center.lng}`,
        z: value.zoom,
        l: value.layers.join(',')
      }),
      deserialize: (value) => ({
        center: {
          lat: parseFloat(value.c.split(',')[0]),
          lng: parseFloat(value.c.split(',')[1])
        },
        zoom: parseInt(value.z),
        layers: value.l.split(',')
      })
    }
  );

  const handleMapMove = (newCenter: { lat: number; lng: number }) => {
    setMapState(prev => ({
      ...prev,
      center: newCenter
    }));
  };

  const toggleLayer = (layer: string) => {
    setMapState(prev => ({
      ...prev,
      layers: prev.layers.includes(layer)
        ? prev.layers.filter(l => l !== layer)
        : [...prev.layers, layer]
    }));
  };

  return (
    <div>
      {/* Map implementation */}
    </div>
  );
}