add useSessionState and persist the maximized panel per tab
This commit is contained in:
@@ -202,6 +202,7 @@ const { jobs, isLoading } = useJobs();
|
||||
| Shared UI state | `useGlobal` |
|
||||
| URL-driven state (filters, pagination) | `useQueryState` |
|
||||
| Component-only state | `useState` |
|
||||
| Shared UI state that must survive a refresh, per tab | `useSessionState` |
|
||||
| Persisted to localStorage | `useLocalStorageState` |
|
||||
|
||||
## Event Handlers
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import { useSessionState, writeSessionValue } from 'hooks/useSessionState';
|
||||
|
||||
type TitleRule = { match: (p: string) => boolean; title: string };
|
||||
|
||||
@@ -51,12 +51,16 @@ export function titleForPath(pathname: string): string {
|
||||
// **sessionStorage is already the per-tab store.** It is separate per tab, survives a refresh and
|
||||
// in-place navigation, and is discarded when the tab closes, which is exactly the lifetime of a tab
|
||||
// name. localStorage would be wrong in the obvious way: every tab would share one name.
|
||||
//
|
||||
// `useSessionState` is that pairing as a hook (see `hooks/useSessionState`); the name is one of its
|
||||
// consumers. The tab *id* below stays on raw storage because it is claimed at module load, before any
|
||||
// component exists to hold it.
|
||||
|
||||
const TAB_LABEL_KEY = 'OFFICER_TAB_LABEL';
|
||||
const TAB_ID_KEY = 'OFFICER_TAB_ID';
|
||||
const IDENTITY_CHANNEL = 'officer-tab-identity';
|
||||
|
||||
/** Storage can throw outright (private mode, storage disabled). A tab name is not worth a crash. */
|
||||
/** Storage can throw outright (private mode, storage disabled). A tab id is not worth a crash. */
|
||||
function readStored(key: string): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(key);
|
||||
@@ -65,18 +69,14 @@ function readStored(key: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function writeStored(key: string, value: string | null): void {
|
||||
function writeStored(key: string, value: string): void {
|
||||
try {
|
||||
if (value) sessionStorage.setItem(key, value);
|
||||
else sessionStorage.removeItem(key);
|
||||
sessionStorage.setItem(key, value);
|
||||
} catch {
|
||||
/* the value just doesn't persist */
|
||||
}
|
||||
}
|
||||
|
||||
const readTabLabel = (): string | null => readStored(TAB_LABEL_KEY);
|
||||
const writeTabLabel = (label: string | null): void => writeStored(TAB_LABEL_KEY, label);
|
||||
|
||||
const labelDroppedHandlers = new Set<() => void>();
|
||||
|
||||
/** The clone check answers late, so React may already be showing the inherited name when it lands. */
|
||||
@@ -127,7 +127,7 @@ function claimTabIdentity(): void {
|
||||
// Someone alive is already this tab, so we are the copy. Take a new id and give up the name.
|
||||
tabId = newTabId();
|
||||
writeStored(TAB_ID_KEY, tabId);
|
||||
writeTabLabel(null);
|
||||
writeSessionValue<string | null>(TAB_LABEL_KEY, null);
|
||||
for (const handler of labelDroppedHandlers) handler();
|
||||
};
|
||||
channel.postMessage({ kind: 'claim', tabId });
|
||||
@@ -151,23 +151,11 @@ claimTabIdentity();
|
||||
*/
|
||||
export function usePageTitle() {
|
||||
const { pathname } = useLocation();
|
||||
const [label, setLabel] = useGlobal<string | null>('TAB_LABEL', readTabLabel);
|
||||
const [label, setLabel] = useSessionState<string | null>(TAB_LABEL_KEY, null);
|
||||
|
||||
// `setLabel` is rebuilt every render, so it is read through a ref rather than listed as a dependency —
|
||||
// as a dependency it would tear the subscription down and rebuild it on every render.
|
||||
const setLabelRef = useRef(setLabel);
|
||||
setLabelRef.current = setLabel;
|
||||
useEffect(() => onTabLabelDropped(() => setLabelRef.current(null)), []);
|
||||
useEffect(() => onTabLabelDropped(() => setLabel(null)), [setLabel]);
|
||||
|
||||
// The React Query copy is what re-renders every consumer; sessionStorage is what survives the reload.
|
||||
const rename = useCallback(
|
||||
(next: string) => {
|
||||
const trimmed = next.trim() || null;
|
||||
writeTabLabel(trimmed);
|
||||
setLabel(trimmed);
|
||||
},
|
||||
[setLabel],
|
||||
);
|
||||
const rename = useCallback((next: string) => setLabel(next.trim() || null), [setLabel]);
|
||||
|
||||
return [label ?? titleForPath(pathname), rename] as const;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user