Releases: nitedani/zustand-querystring
v0.6.0
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
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
initialStatehints 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_cstays asa_b_c(nota__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%3A5After (v0.5.0):
// Default: key: false (standalone mode)
// URL: ?count=5To 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
What's New
- Standalone mode: Set
key: falseto 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
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
keydocumentation ($→'state')
Full Changelog: v0.3.0...v0.3.1
v0.3.0
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=JohnBug 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
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.