mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
97 lines
3.1 KiB
JavaScript
97 lines
3.1 KiB
JavaScript
/*
|
|
* Copyright (c) 2026 by Christian Kellner.
|
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
|
*/
|
|
|
|
import { useCallback } from 'react';
|
|
|
|
// Preset parsers for common types
|
|
export const parseString = {
|
|
parse: (v) => v,
|
|
stringify: (v) => v,
|
|
};
|
|
|
|
export const parseNumber = {
|
|
parse: (v) => Number(v),
|
|
stringify: (v) => String(v),
|
|
};
|
|
|
|
export const parseBoolean = {
|
|
parse: (v) => v === 'true',
|
|
stringify: (v) => String(v),
|
|
};
|
|
|
|
// For state that is null | true | false
|
|
export const parseNullableBoolean = {
|
|
parse: (v) => (v === 'true' ? true : v === 'false' ? false : null),
|
|
stringify: (v) => (v === null ? null : String(v)),
|
|
};
|
|
|
|
/**
|
|
* Drop-in replacement for useState that syncs with URL search params.
|
|
* Uses replace: true so filter changes don't add browser history entries.
|
|
*
|
|
* Requires a shared [searchParams, setSearchParams] pair from a single
|
|
* useSearchParams() call in the component. This ensures multiple hooks
|
|
* in the same component don't overwrite each other's params.
|
|
*
|
|
* @param {[URLSearchParams, Function]} searchParamsPair - from useSearchParams()
|
|
* @param {string} key - URL search param key
|
|
* @param {*} defaultValue - value when param is absent
|
|
* @param {{ parse: (s: string) => *, stringify: (v: *) => string|null }} [options]
|
|
*/
|
|
// WeakMap to store pending batched updates per setSearchParams function.
|
|
// This lets multiple useSearchParamState hooks on the same component batch
|
|
// their changes into a single setSearchParams call, preventing them from
|
|
// overwriting each other.
|
|
const pendingUpdates = new WeakMap();
|
|
|
|
export function useSearchParamState([searchParams, setSearchParams], key, defaultValue, options = {}) {
|
|
const { parse = (v) => v, stringify = (v) => String(v) } = options;
|
|
|
|
const rawValue = searchParams.get(key);
|
|
const value = rawValue !== null ? parse(rawValue) : defaultValue;
|
|
|
|
const setValue = useCallback(
|
|
(newValue) => {
|
|
// Collect the change
|
|
if (!pendingUpdates.has(setSearchParams)) {
|
|
pendingUpdates.set(setSearchParams, new Map());
|
|
|
|
// Schedule a single flush at the end of the current microtask
|
|
queueMicrotask(() => {
|
|
const updates = pendingUpdates.get(setSearchParams);
|
|
pendingUpdates.delete(setSearchParams);
|
|
if (!updates || updates.size === 0) return;
|
|
|
|
setSearchParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
for (const [k, entry] of updates) {
|
|
if (entry.remove) {
|
|
next.delete(k);
|
|
} else {
|
|
next.set(k, entry.serialized);
|
|
}
|
|
}
|
|
return next;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
});
|
|
}
|
|
|
|
const batch = pendingUpdates.get(setSearchParams);
|
|
const serialized = stringify(newValue);
|
|
if (newValue === defaultValue || newValue === null || newValue === undefined || serialized === null) {
|
|
batch.set(key, { remove: true });
|
|
} else {
|
|
batch.set(key, { remove: false, serialized });
|
|
}
|
|
},
|
|
[key, defaultValue, stringify, setSearchParams],
|
|
);
|
|
|
|
return [value, setValue];
|
|
}
|