export interface QueryCodec<T> {
parse: (raw: string | null) => T;
serialize: (value: T) => string | null;
}
export function stringParam(fallback = ""): QueryCodec<string> {
return {
parse: (raw) => raw ?? fallback,
serialize: (value) => (value === fallback ? null : value),
};
}
export function numberParam(fallback: number): QueryCodec<number> {
return {
parse: (raw) => {
if (raw === null) return fallback;
const n = Number(raw);
return Number.isNaN(n) ? fallback : n;
},
serialize: (value) => (value === fallback ? null : String(value)),
};
}
export function booleanParam(fallback = false): QueryCodec<boolean> {
return {
parse: (raw) => (raw === null ? fallback : raw === "true"),
serialize: (value) => (value === fallback ? null : String(value)),
};
}
export function enumParam<T extends string>(
allowed: readonly T[],
fallback: T
): QueryCodec<T> {
return {
parse: (raw) => (allowed.includes(raw as T) ? (raw as T) : fallback),
serialize: (value) => (value === fallback ? null : value),
};
}
import { useCallback, useEffect, useSyncExternalStore } from "react";
import type { QueryCodec } from "./queryCodecs";
type Updater<T> = T | ((prev: T) => T);
function subscribe(onChange: () => void): () => void {
window.addEventListener("popstate", onChange);
return () => window.removeEventListener("popstate", onChange);
}
function getSearchString(): string {
return window.location.search;
}
export function useQueryState<T>(
key: string,
codec: QueryCodec<T>,
options: { replace?: boolean } = {}
): [T, (next: Updater<T>) => void] {
const search = useSyncExternalStore(subscribe, getSearchString, () => "");
const value = codec.parse(new URLSearchParams(search).get(key));
const setValue = useCallback(
(next: Updater<T>) => {
const params = new URLSearchParams(window.location.search);
const current = codec.parse(params.get(key));
const resolved =
typeof next === "function"
? (next as (prev: T) => T)(current)
: next;
const encoded = codec.serialize(resolved);
if (encoded === null) {
params.delete(key);
} else {
params.set(key, encoded);
}
const qs = params.toString();
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
if (options.replace) {
window.history.replaceState(null, "", url);
} else {
window.history.pushState(null, "", url);
}
window.dispatchEvent(new PopStateEvent("popstate"));
},
[key, codec, options.replace]
);
useEffect(() => () => {}, []);
return [value, setValue];
}
import { useMemo } from "react";
import { debounce } from "lodash";
import { useQueryState } from "./useQueryState";
import { stringParam, numberParam, booleanParam, enumParam } from "./queryCodecs";
const SORTS = ["relevance", "price_asc", "price_desc"] as const;
export function FilterBar() {
const [search, setSearch] = useQueryState("q", stringParam(""), { replace: true });
const [sort, setSort] = useQueryState("sort", enumParam(SORTS, "relevance"));
const [inStock, setInStock] = useQueryState("stock", booleanParam(false));
const [page, setPage] = useQueryState("page", numberParam(1));
const pushSearch = useMemo(
() => debounce((v: string) => setSearch(v), 300),
[setSearch]
);
return (
<div className="filter-bar">
<input
type="search"
defaultValue={search}
placeholder="Search products"
onChange={(e) => {
setPage(1);
pushSearch(e.target.value);
}}
/>
<select value={sort} onChange={(e) => setSort(e.target.value as typeof SORTS[number])}>
{SORTS.map((s) => (
<option key={s} value={s}>{s.replace("_", " ")}</option>
))}
</select>
<label>
<input
type="checkbox"
checked={inStock}
onChange={(e) => {
setPage(1);
setInStock(e.target.checked);
}}
/>
In stock only
</label>
<button disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>Prev</button>
<span>Page {page}</span>
<button onClick={() => setPage((p) => p + 1)}>Next</button>
</div>
);
}
This snippet shows how filter/form state can live in the URL query string instead of component state, so a page is shareable, bookmarkable, and survives a refresh. The core idea is a small codec layer plus a typed hook that reads and writes URLSearchParams while staying framework-agnostic about how the URL actually changes.
The queryCodecs tab defines the serialization contract. A QueryCodec<T> is just a parse/serialize pair, and factory functions like stringParam, numberParam, enumParam, and booleanParam produce codecs with sensible fallbacks. Keeping parsing here matters because query strings are always strings and always untrusted — a user can hand-edit ?page=banana, so numberParam returns a fallback when Number yields NaN rather than propagating a bad value into the UI. enumParam restricts values to a known set, which is exactly what typed filters need.
The useQueryState hook tab wires a codec to a single query key. It reads the live URLSearchParams via a getSearchString accessor and subscribes to popstate so browser back/forward stay in sync. The returned setValue supports a functional updater like useState, computes the next params, and drops the key entirely when the value equals the codec's default via serialize returning null — this keeps URLs clean instead of accumulating ?sort=default. Writes go through an injected navigate callback so the hook works with the History API, React Router, or Next.js without hard-coding one.
The FilterBar component tab composes several useQueryState calls into a real filter form. Note the search field uses debounce from lodash so each keystroke does not spam history.pushState, while discrete controls like the sort dropdown and the in-stock checkbox update immediately. Because every control is backed by the URL, the parent list component can derive its query purely from useSearchParams, and no duplicate useState is needed.
The main trade-offs: URL state is stringly-typed and size-limited, so it suits filters and pagination rather than large or sensitive data, and rapid updates should be debounced or use replace to avoid polluting history. The codec pattern isolates all of that fragility in one tested place.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import SwiftUI
struct ContentView: View {
@State private var username = ""
@State private var isLoggedIn = false
@StateObject private var viewModel = LoginViewModel()
SwiftUI declarative UI with state management
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.