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

@vinctus/querystate

v0.2.2

Published

URL query parameters as React state

Downloads

68

Readme

QueryState

npm version npm downloads License: ISC TypeScript React

A lightweight, type-safe React library for managing URL query parameters as application state, with support for strings, numbers, booleans, dates, arrays, and fixed-length tuples.

Features

  • URL-Synchronized State - Persist UI state directly in the URL without manual synchronization
  • Type-Safe API - Fully TypeScript-compatible with intuitive types for values and setters
  • Chainable Configuration - Fluent API for defining parameter types with defaults
  • Array Support - First-class handling of multi-select components and array parameters
  • Number Values - Native support for numeric parameters with proper type conversion
  • Tuple Support - Fixed-length arrays that maintain their structure (perfect for sliders, coordinates, colors)
  • Default Values - Optional defaults that automatically populate the URL when parameters are missing
  • React Router Integration - Built on React Router's useSearchParams for seamless compatibility
  • Framework Agnostic - Works with any UI component library that accepts string/array values

Installation

npm install @vinctus/querystate react-router-dom

Basic Usage

import { useQueryState, queryState } from '@vinctus/querystate';
import { Select, InputNumber, Slider, Switch, DatePicker } from 'antd';

function FilterComponent() {
  const {
    category, setCategory,        // String parameter (string | undefined)
    tags, setTags,                // String array (string[])
    page, setPage,                // Number parameter with default (number - never undefined)
    priceRange, setPriceRange,    // Number tuple with default ([number, number] - never undefined)
    isActive, setIsActive,        // Boolean with default (boolean - never undefined)
    startDate, setStartDate        // Date parameter (Date | undefined)
  } = useQueryState({
    category: queryState.string(),
    tags: queryState.string().array(),
    page: queryState.number().default(1),
    priceRange: queryState.number().tuple(2).default([0, 100]),
    isActive: queryState.boolean().default(true),
    startDate: queryState.date()
  });

  return (
          <div>
            <Select
                    placeholder="Select category"
                    value={category}  // May be undefined if not in URL
                    onChange={setCategory}
                    options={[
                      { value: 'electronics', label: 'Electronics' },
                      { value: 'books', label: 'Books' }
                    ]}
            />

            <Select
                    mode="multiple"
                    placeholder="Select tags"
                    value={tags}  // Empty array if not in URL, never undefined
                    onChange={setTags}
                    options={[
                      { value: 'new', label: 'New' },
                      { value: 'sale', label: 'Sale' }
                    ]}
            />

            <InputNumber
                    value={page}  // Always a number (default: 1), never undefined
                    onChange={setPage}
                    min={1}
            />

            <Slider
                    range
                    min={0}
                    max={1000}
                    value={priceRange}  // Always [number, number] (default: [0, 100]), never undefined
                    onChange={setPriceRange}
            />

            <Switch
                    checked={isActive}  // Always boolean (default: true), never undefined
                    onChange={setIsActive}
            />

            <DatePicker
                    value={startDate}  // May be undefined if not in URL
                    onChange={setStartDate}
            />
          </div>
  );
}

Parameter Types

Here's what each parameter type returns based on whether it has a default value:

String Parameters

// Without default: string | undefined
const { name, setName } = useQueryState({
  name: queryState.string()
});
// name: string | undefined

// With default: string (never undefined)
const { status, setStatus } = useQueryState({
  status: queryState.string().default('active')
});
// status: string

Array Parameters

// Array without default: always returns [] when not in URL (never undefined)
const { tags, setTags } = useQueryState({
  tags: queryState.string().array()
});
// tags: string[]

// Array with default: uses default values when not in URL
const { priority, setPriority } = useQueryState({
  priority: queryState.string().array().default(['medium'])
});
// priority: string[]

Number Parameters

// Without default: number | undefined
const { id, setId } = useQueryState({
  id: queryState.number()
});
// id: number | undefined

// With default: number (never undefined)
const { page, setPage } = useQueryState({
  page: queryState.number().default(1)
});
// page: number

Boolean Parameters

// Without default: boolean | undefined
const { isPublished, setIsPublished } = useQueryState({
  isPublished: queryState.boolean()
});
// isPublished: boolean | undefined

// With default: boolean (never undefined)
const { isActive, setIsActive } = useQueryState({
  isActive: queryState.boolean().default(true)
});
// isActive: boolean

Date Parameters

// Without default: Date | undefined
const { createdAt, setCreatedAt } = useQueryState({
  createdAt: queryState.date()
});
// createdAt: Date | undefined

// With default: Date (never undefined)
const { startDate, setStartDate } = useQueryState({
  startDate: queryState.date().default(new Date())
});
// startDate: Date

// Date with constraints
const { eventDate, setEventDate } = useQueryState({
  eventDate: queryState.date().future()  // Must be in the future
});

Tuple Parameters

// Tuples are always fixed-length and never undefined
const { priceRange, setPriceRange } = useQueryState({
  priceRange: queryState.number().tuple(2).default([0, 100])
});
// priceRange: [number, number]

// String tuples with defaults
const { fullName, setFullName } = useQueryState({
  fullName: queryState.string().tuple(2).default(['John', 'Doe'])
});
// fullName: [string, string]

// Date tuples (for date ranges)
const { dateRange, setDateRange } = useQueryState({
  dateRange: queryState.date().tuple(2).default([new Date('2024-01-01'), new Date('2024-12-31')])
});
// dateRange: [Date, Date]

// Without an explicit default, tuples may be undefined
const { coordinates, setCoordinates } = useQueryState({
  coordinates: queryState.number().tuple(2)
});
// coordinates: [number, number] | undefined

Type Safety Benefits

Understanding the type behavior of QueryState has important implications for your application code:

1. No Nullability Checks Needed for Defaults

When you provide a default value, you don't need optional chaining or nullability checks:

// With default - no need for optional chaining or nullability checks
const { page } = useQueryState({
  page: queryState.number().default(1)
});

// Safe to use directly - will never be undefined
const nextPage = page + 1;

2. Proper Type Guards for Non-Default Values

For parameters without defaults, use proper type guards:

const { category } = useQueryState({
  category: queryState.string()
});

// Need to check for undefined
if (category) {
  // Safe to use category as string here
  console.log(category.toUpperCase());
}

3. Arrays and Tuples Are Always Available

Array parameters (both with and without defaults) are never undefined:

const { tags, coordinates } = useQueryState({
  tags: queryState.string().array(),
  coordinates: queryState.number().tuple(2)
});

// Safe to use array methods directly
const hasSaleTag = tags.includes('sale');

// Safe to access tuple elements directly
const [x, y] = coordinates;

Working with Tuples vs Arrays

Variable-Length Arrays

Array parameters are variable-length and can be empty:

  • The URL might show ?scores=85&scores=92&scores=78 for multiple values
  • If all values are removed, the parameter disappears from the URL
  • Arrays provide flexibility when the number of items can vary
// Array can have any number of items (including zero)
const { productIds, setProductIds } = useQueryState({
  productIds: queryState.number().array()
});

// Adding/removing elements
setProductIds([...productIds, 1005]);  // Add a value
setProductIds(productIds.filter(id => id !== 1002));  // Remove a value
setProductIds([]);  // Clear all values

Fixed-Length Tuples

Tuple parameters always maintain exactly the specified number of elements:

  • The URL for a color might show ?rgbColor=255&rgbColor=128&rgbColor=0
  • If some values are manually removed from the URL, they're restored with defaults
  • Perfect for parameters that require a specific structure (coordinates, ranges, colors)
// Tuple always has exactly 2 elements
const { priceRange, setPriceRange } = useQueryState({
  priceRange: queryState.number().tuple(2).default([0, 100])
});

// Modifying just one element
const newRange = [...priceRange];
newRange[0] = 25;  // Update min value
setPriceRange(newRange);

Why Use QueryState?

  • Simplifies State Management: Eliminates duplicate state between URL and React components
  • Improves UX: Enables shareable, bookmarkable filters and views through URL persistence
  • Type Safety: Provides correct TypeScript types for parameters (strings, numbers, arrays, tuples)
  • UI Library Compatible: Works with any component library including Ant Design, MUI, etc.
  • Consistent Parameter Handling: Properly formats array parameters in the URL
  • Automatic Default Values: Ensures sensible defaults without extra code
  • Structure Preservation: Tuples maintain their structure even with URL manipulation

URL Format Examples

QueryState maintains proper URL formatting:

  • Single string: ?category=electronics
  • Single number: ?page=3
  • String array: ?tags=new&tags=sale
  • Number array: ?scores=85&scores=92&scores=78
  • Number tuple: ?priceRange=10&priceRange=90

This format ensures compatibility with server-side processing, browser history, and bookmarking.

Requirements

  • React 16.8+ (for Hooks)
  • React Router DOM 6.0+
  • TypeScript 5.0+ (recommended)

License

ISC