mirror of
https://github.com/block/buzz.git
synced 2026-11-15 05:41:14 +00:00
Signed-off-by: tho <80c5f18be5aafa62cf6198c6335963ba3306b595288117c8ea2f805fc9bdc94a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Co-authored-by: tho <80c5f18be5aafa62cf6198c6335963ba3306b595288117c8ea2f805fc9bdc94a@sprout-oss.stage.blox.sqprod.co>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import * as React from "react";
|
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
|
|
|
/**
|
|
* URL-search-param-backed UI state, so it lives in the history stack:
|
|
* back/forward restores the value a given entry carried, and reloads
|
|
* restore it from the URL.
|
|
*
|
|
* Patch calls made synchronously within one event handler are coalesced
|
|
* into a single navigation, so each user action produces exactly one
|
|
* history entry even when a handler updates several keys.
|
|
*/
|
|
|
|
export type HistorySearchSetterOptions = {
|
|
/** Rewrite the current entry instead of pushing a new one. */
|
|
replace?: boolean;
|
|
};
|
|
|
|
export function useHistorySearchState<K extends string>(keys: readonly K[]) {
|
|
const navigate = useNavigate();
|
|
const search = useSearch({ strict: false } as never) as Partial<
|
|
Record<K, string>
|
|
>;
|
|
|
|
const values = {} as Record<K, string | null>;
|
|
for (const key of keys) {
|
|
values[key] = search[key] ?? null;
|
|
}
|
|
|
|
const currentValuesRef = React.useRef(values);
|
|
currentValuesRef.current = values;
|
|
const keysRef = React.useRef(keys);
|
|
keysRef.current = keys;
|
|
|
|
const pendingRef = React.useRef<{
|
|
patch: Partial<Record<K, string | null>>;
|
|
replace: boolean;
|
|
} | null>(null);
|
|
|
|
const applyPatch = React.useCallback(
|
|
(
|
|
patch: Partial<Record<K, string | null>>,
|
|
options?: HistorySearchSetterOptions,
|
|
) => {
|
|
const pending = pendingRef.current;
|
|
if (pending) {
|
|
Object.assign(pending.patch, patch);
|
|
pending.replace = pending.replace || Boolean(options?.replace);
|
|
return;
|
|
}
|
|
|
|
pendingRef.current = {
|
|
patch: { ...patch },
|
|
replace: Boolean(options?.replace),
|
|
};
|
|
queueMicrotask(() => {
|
|
const flush = pendingRef.current;
|
|
pendingRef.current = null;
|
|
if (!flush) {
|
|
return;
|
|
}
|
|
|
|
const currentValues = currentValuesRef.current;
|
|
const isChanged = keysRef.current.some(
|
|
(key) =>
|
|
flush.patch[key] !== undefined &&
|
|
(flush.patch[key] ?? null) !== currentValues[key],
|
|
);
|
|
if (!isChanged) {
|
|
return;
|
|
}
|
|
|
|
void navigate({
|
|
to: ".",
|
|
search: (previousSearch: Record<string, unknown>) => {
|
|
const nextSearch = { ...previousSearch };
|
|
for (const key of keysRef.current) {
|
|
const value = flush.patch[key];
|
|
if (value === undefined) {
|
|
continue;
|
|
}
|
|
if (value === null) {
|
|
delete nextSearch[key];
|
|
} else {
|
|
nextSearch[key] = value;
|
|
}
|
|
}
|
|
return nextSearch;
|
|
},
|
|
replace: flush.replace,
|
|
resetScroll: false,
|
|
} as never);
|
|
});
|
|
},
|
|
[navigate],
|
|
);
|
|
|
|
return { applyPatch, values };
|
|
}
|