Skip to content

Releases: nitedani/zustand-querystring

v0.6.0

14 Feb 03:54

Choose a tag to compare

v0.6.0

Bug Fixes

Comma-separated arrays not restored from URL
When using the plain format with dynamic keys (e.g., Record<string, string[]> with initialState: { filters: {} }), comma-separated values like filters.gpus_.architecture=Blackwell,Ada were restored as the single string "Blackwell,Ada" instead of ["Blackwell", "Ada"]. The parser now splits on unescaped array separators regardless of whether a type hint exists in initialState. Escaped separators (_,) are correctly preserved.

Dynamic keys not cleared from URL
When resetting a dynamic object back to {} (e.g., clearing all filters), its URL params were not removed. The middleware now tracks which keys it wrote on the previous update, so params that disappear between state transitions are correctly cleaned up.

Breaking Changes

Plain format default arraySeparator changed from ',' to 'repeat'

The default plain format now uses repeated keys for arrays:

# Before (v0.5.0): comma-separated
?tags=a,b,c

# After (v0.6.0): repeated keys
?tags=a&tags=b&tags=c

This eliminates ambiguity between scalars and single-element arrays when using dynamic keys. With 'repeat', the number of key occurrences in the URL directly encodes whether a value is a scalar or an array — no type hint needed.

To restore the old behavior:

import { createFormat } from 'zustand-querystring/format/plain';
const format = createFormat({ arraySeparator: ',' });

Note: With arraySeparator: ',' and dynamic keys (initialState: {}), a single array value like os=CentOS is indistinguishable from a scalar string. Use 'repeat' for dynamic keys, or normalize with Array.isArray(val) ? val : [val].

v0.5.0

14 Dec 22:57

Choose a tag to compare

v0.5.0

What's New

New plain Format

A new human-readable format for cleaner, hand-editable URLs:

import { plain } from 'zustand-querystring/format/plain';

const useStore = create()(
  querystring(
    (set) => ({
      query: "hello",
      filters: { sort: "name" },
      tags: ["a", "b"]
    }),
    {
      key: false,
      format: plain,
      select: () => ({ query: true, filters: true, tags: true }),
    }
  )
);

Result: ?query=hello&filters.sort=name&tags=a&tags=b

Features:

  • Dot notation for nested objects: filters.sort=name
  • Repeated keys for arrays: tags=a&tags=b
  • Fully customizable separators via createFormat()

Limitation: The plain format requires initialState for proper type inference. Since URL params are strings, the format uses your initial state to determine types:

  • Arrays are detected from initial state (otherwise multiple values become last-value-wins)
  • Numbers, booleans, and dates are auto-parsed, but initialState hints ensure correct types
  • Without initialState, values like "123" may be parsed as numbers automatically

Interactive Playground

New live playground to experiment with formats and options:
👉 https://zustand-querystring.nitedani.workers.dev

Improved Escaping

  • Escape character changed from / to _ for better URL compatibility
  • Smarter escaping: only special characters are escaped, regular underscores stay as-is
  • Example: a_b_c stays as a_b_c (not a__b__c)

Breaking Changes

key: false is now the default

The default value of key has changed from 'state' to false.

Before (v0.4.0):

// Default: key: 'state'
// URL: ?state=count%3A5

After (v0.5.0):

// Default: key: false (standalone mode)
// URL: ?count=5

To restore the old behavior, explicitly set key: 'state':

querystring(store, { key: 'state', ... })

Escape character changed

Default escape character changed from / to _ in both marked and plain formats.

readable format renamed to marked

The readable format has been renamed to marked for clarity.

// Before
import { readable } from 'zustand-querystring/format/readable';

// After
import { marked } from 'zustand-querystring/format/marked';

Full Changelog: v0.4.0...v0.5.0

v0.4.0

22 Nov 14:24

Choose a tag to compare

What's New

  • Standalone mode: Set key: false to use individual query parameters for each state key. Allows multiple stores on the same page without conflicts.

Breaking Changes

  • Readable format changes: The readable format has been updated with breaking changes to the encoding scheme.

v0.3.1

21 Nov 10:42

Choose a tag to compare

v0.3.1

New Features

syncNull and syncUndefined Options

Control how null and undefined values are handled:

querystring(storeInitializer, {
  syncNull: true,      // Sync null values to URL (default: false)
  syncUndefined: true, // Sync undefined values to URL (default: false)
})

Default (backward compatible): null/undefined values are not synced - setting them clears back to initial state

When enabled: null/undefined values persist in URL across refreshes

Changes

  • Only plain objects ({}) are recursively compared - Date, RegExp, Map, Set, and class instances are atomic
  • Added 45+ comprehensive tests
  • Updated documentation with usage examples and behavior clarifications
  • Fixed default key documentation ($'state')

Full Changelog: v0.3.0...v0.3.1

v0.3.0

21 Nov 08:39

Choose a tag to compare

What's New

Human-Readable URL Format

Added an optional readable format for cleaner, more compact query strings:

import * as format from "zustand-querystring/format/readable";

const useStore = create()(
  querystring(
    (set) => ({ count: 0, name: "John" }),
    { format }
  )
);

Before: ?state=%7B%22count%22%3A5%2C%22name%22%3A%22John%22%7D
After: ?state=count:5..name=John

Bug Fixes

  • Fixed double-encoding issue where query parameters were encoded twice

Other Changes

  • Added comprehensive test suite with 50+ edge case tests
  • Updated documentation

Breaking Changes

  • Fixed double-encoding bug. URLs from v0.2.0 will not be compatible and will reset to default state.

0.1.0, new URL encoding

08 Aug 01:33
68a9dac

Choose a tag to compare

0.1.0 is a breaking change.
The URL encoding and decoding is simplified.
Before 0.1.0, zustand-querystring used a proprietary format to encode the state in the URL.
While this made the URL more readable and short, it could cause unexpected issues when other tools/libraries tried to parse it.
In this new version, encoding and decoding is handled by encodeURIComponent and decodeURIComponent. This makes the state in the URL unreadable, but should be more compatible with third-party tools.