merge: ui/remit-drawer
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
// @vitest-environment happy-dom
|
||||
// Mirror the IS_REACT_ACT_ENVIRONMENT setup from useClaimDetail.test.ts so
|
||||
// React doesn't log act() warnings about the createRoot render/unmount.
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useRemitDetail } from "./useRemitDetail";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
import type { Remittance } from "@/types";
|
||||
|
||||
// Module-level mock: vitest hoists `vi.mock` above imports. We mock both
|
||||
// `api.getRemittance` (the only method the hook calls) AND `ApiError`
|
||||
// (the class used in the retry predicate) so that `instanceof ApiError`
|
||||
// in the hook and `new ApiError(404, ...)` in the test resolve to the
|
||||
// same class at runtime.
|
||||
vi.mock("@/lib/api", () => {
|
||||
class ApiError extends Error {
|
||||
constructor(public status: number, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
return {
|
||||
api: {
|
||||
getRemittance: vi.fn(),
|
||||
},
|
||||
ApiError,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Minimal renderHook shim — same pattern as `useClaimDetail.test.ts`.
|
||||
* The project doesn't ship `@testing-library/react`, so we wire a Probe
|
||||
* component into a real `createRoot` and read its return value through
|
||||
* a shared `result` object.
|
||||
*/
|
||||
function renderHook<TResult>(
|
||||
useCallback: () => TResult
|
||||
): {
|
||||
result: { current: TResult | undefined };
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: { current: TResult | undefined } = { current: undefined };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
|
||||
function Probe() {
|
||||
result.current = useCallback();
|
||||
return null;
|
||||
}
|
||||
|
||||
const root: Root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
React.createElement(QueryClientProvider, { client: qc }, React.createElement(Probe))
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Poll the result until `predicate` is true or we time out. */
|
||||
async function waitForState(
|
||||
predicate: () => boolean,
|
||||
timeoutMs = 1000
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (!predicate()) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
throw new Error(`waitForState: predicate did not hold within ${timeoutMs}ms`);
|
||||
}
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal valid Remittance — every required key present on the base
|
||||
// `Remittance` interface. Mirrors the shape used by the page-level
|
||||
// tests in `Remittances.test.tsx` so the type-checks line up with the
|
||||
// rest of the codebase.
|
||||
const SAMPLE_DETAIL: Remittance = {
|
||||
id: "REM-1",
|
||||
claimId: "CLM-1",
|
||||
payerName: "Medicaid",
|
||||
paidAmount: 100.0,
|
||||
adjustmentAmount: 20.0,
|
||||
receivedDate: "2026-01-15T00:00:00Z",
|
||||
checkNumber: "EFT-12345",
|
||||
status: "received",
|
||||
};
|
||||
|
||||
describe("useRemitDetail", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("returns the empty drawer state and does not fetch when id is null", () => {
|
||||
// The drawer is closed → no network call, no loading, no error.
|
||||
// The hook must short-circuit BEFORE the queryFn is ever invoked.
|
||||
const { result, unmount } = renderHook(() => useRemitDetail(null));
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(result.current?.data).toBeNull();
|
||||
expect(result.current?.isLoading).toBe(false);
|
||||
expect(result.current?.isError).toBe(false);
|
||||
expect(result.current?.error).toBeNull();
|
||||
expect(typeof result.current?.refetch).toBe("function");
|
||||
expect(api.getRemittance).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("fetches the remit and returns the parsed body for a valid id", async () => {
|
||||
(api.getRemittance as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
SAMPLE_DETAIL
|
||||
);
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDetail("REM-1"));
|
||||
|
||||
await waitForState(
|
||||
() =>
|
||||
result.current?.data !== null &&
|
||||
result.current?.data !== undefined &&
|
||||
result.current?.isLoading === false
|
||||
);
|
||||
|
||||
expect(result.current?.data).toEqual(SAMPLE_DETAIL);
|
||||
expect(result.current?.isError).toBe(false);
|
||||
expect(result.current?.error).toBeNull();
|
||||
expect(api.getRemittance).toHaveBeenCalledTimes(1);
|
||||
expect(api.getRemittance).toHaveBeenCalledWith("REM-1");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("surfaces a 404 ApiError as isError=true with the original error object", async () => {
|
||||
// The hook's retry predicate must give up immediately on 404 — the
|
||||
// drawer needs to render the "remit doesn't exist" state rather than
|
||||
// masking it with three back-to-back retries.
|
||||
(api.getRemittance as unknown as ReturnType<typeof vi.fn>).mockRejectedValue(
|
||||
new ApiError(404, "Remittance ghost not found")
|
||||
);
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDetail("ghost"));
|
||||
|
||||
await waitForState(() => result.current?.isError === true);
|
||||
|
||||
expect(result.current?.data).toBeNull();
|
||||
expect(result.current?.isLoading).toBe(false);
|
||||
expect(result.current?.isError).toBe(true);
|
||||
expect(result.current?.error).toBeInstanceOf(ApiError);
|
||||
expect((result.current?.error as ApiError).status).toBe(404);
|
||||
// 404s must not retry — exactly one fetch attempt.
|
||||
expect(api.getRemittance).toHaveBeenCalledTimes(1);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
import type { Remittance } from "@/types";
|
||||
|
||||
/**
|
||||
* UI-facing remittance detail shape returned by `GET /api/remittances/{id}`.
|
||||
*
|
||||
* The backend mapper (`cyclone.store.to_ui_remittance_with_adjustments`)
|
||||
* returns the standard `Remittance` fields plus the labeled `adjustments`
|
||||
* array. We extend the base type locally with the additional 835-derived
|
||||
* fields the backend emits — `payerClaimControlNumber` and `totalCharge`
|
||||
* (which aren't part of the v1 `Remittance` interface but ARE part of the
|
||||
* backend's `/api/remittances/{id}` payload). All newly-added fields are
|
||||
* optional so the component degrades gracefully when the backend hasn't
|
||||
* populated them (e.g. earlier rows without those columns populated).
|
||||
*
|
||||
* `payer` and `payee` blocks are also optional — the current detail
|
||||
* endpoint does not surface these yet, but the PartiesGrid renders them
|
||||
* when present so a future backend version lights up the cards without a
|
||||
* frontend change.
|
||||
*/
|
||||
export interface RemitDetail extends Remittance {
|
||||
payerClaimControlNumber?: string;
|
||||
totalCharge?: number;
|
||||
paymentMethod?: string | null;
|
||||
paymentDate?: string | null;
|
||||
payer?: {
|
||||
name?: string | null;
|
||||
id?: string | null;
|
||||
address?: {
|
||||
line1?: string;
|
||||
line2?: string | null;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zip?: string;
|
||||
} | null;
|
||||
} | null;
|
||||
payee?: {
|
||||
name?: string | null;
|
||||
npi?: string | null;
|
||||
address?: {
|
||||
line1?: string;
|
||||
line2?: string | null;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zip?: string;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-remittance detail drawer query (RemitDrawer).
|
||||
*
|
||||
* Mirror of `useClaimDetail` — same return shape, same retry semantics.
|
||||
* Returns `{ data, isLoading, isError, error, refetch }`:
|
||||
* - `id === null` (drawer closed): the query is disabled and the hook
|
||||
* short-circuits to the empty drawer state so a closed drawer doesn't
|
||||
* burn a network request or a TanStack cache slot.
|
||||
* - `id` is set: fetches `GET /api/remittances/{id}` via
|
||||
* `api.getRemittance`. Cached 30 s so j/k navigation across remits
|
||||
* reuses prior fetches within the drawer session.
|
||||
* - On 404: the hook's retry predicate short-circuits (no retries) so
|
||||
* the drawer's not-found state appears immediately rather than being
|
||||
* masked by three back-to-back retry attempts.
|
||||
*/
|
||||
export function useRemitDetail(id: string | null): {
|
||||
data: RemitDetail | null;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => void;
|
||||
} {
|
||||
const q = useQuery<RemitDetail>({
|
||||
queryKey: ["remit-detail", id],
|
||||
queryFn: () => api.getRemittance<RemitDetail>(id as string),
|
||||
enabled: id !== null,
|
||||
staleTime: 30 * 1000,
|
||||
retry: (failureCount, error) => {
|
||||
if (error instanceof ApiError && error.status === 404) return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
});
|
||||
|
||||
if (id === null) {
|
||||
return {
|
||||
data: null,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: q.data ?? null,
|
||||
isLoading: q.isLoading,
|
||||
isError: q.isError,
|
||||
error: q.error,
|
||||
refetch: () => {
|
||||
void q.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// @vitest-environment happy-dom
|
||||
// Mirror the IS_REACT_ACT_ENVIRONMENT setup from useDrawerUrlState.test.ts
|
||||
// so React doesn't log act() warnings about the createRoot render/unmount
|
||||
// and the popstate-driven state updates.
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { useRemitDrawerUrlState } from "./useRemitDrawerUrlState";
|
||||
|
||||
/**
|
||||
* Minimal renderHook shim — same pattern as the other hook tests.
|
||||
*/
|
||||
function renderHook<TResult>(setup: () => TResult): {
|
||||
result: { current: TResult | undefined };
|
||||
unmount: () => void;
|
||||
} {
|
||||
const result: { current: TResult | undefined } = { current: undefined };
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
|
||||
function Probe() {
|
||||
result.current = setup();
|
||||
return null;
|
||||
}
|
||||
|
||||
const root: Root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(React.createElement(Probe));
|
||||
});
|
||||
|
||||
return {
|
||||
result,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Point happy-dom's URL at a known value. happy-dom v20 doesn't expose a
|
||||
* writable `window.location.search`, but `window.happyDOM.setURL` updates
|
||||
* the URL the window reports without triggering a navigation — exactly
|
||||
* what we want for mounting the hook at `/remittances?remit=REM-1` etc.
|
||||
*/
|
||||
function setLocation(url: string): void {
|
||||
(window as unknown as { happyDOM: { setURL: (u: string) => void } }).happyDOM.setURL(url);
|
||||
}
|
||||
|
||||
describe("useRemitDrawerUrlState", () => {
|
||||
type PushState = (state: unknown, unused: string, url?: string | URL | null) => void;
|
||||
let pushStateMock: ReturnType<typeof vi.fn<PushState>>;
|
||||
let replaceStateMock: ReturnType<typeof vi.fn<PushState>>;
|
||||
|
||||
beforeEach(() => {
|
||||
pushStateMock = vi.fn();
|
||||
replaceStateMock = vi.fn();
|
||||
|
||||
vi.stubGlobal("history", {
|
||||
pushState: pushStateMock,
|
||||
replaceState: replaceStateMock,
|
||||
state: null,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
setLocation("http://localhost/");
|
||||
});
|
||||
|
||||
it("reads the ?remit= param from window.location.search on mount", () => {
|
||||
setLocation("http://localhost/remittances?remit=REM-1");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
expect(result.current?.remitId).toBe("REM-1");
|
||||
expect(typeof result.current?.open).toBe("function");
|
||||
expect(typeof result.current?.close).toBe("function");
|
||||
expect(typeof result.current?.setRemitId).toBe("function");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("returns null remitId when no ?remit= param is set", () => {
|
||||
setLocation("http://localhost/remittances");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
expect(result.current?.remitId).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("returns null remitId when ?remit= is present but empty", () => {
|
||||
setLocation("http://localhost/remittances?remit=");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
expect(result.current?.remitId).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("open(id) pushes a new history entry containing ?remit=id", () => {
|
||||
setLocation("http://localhost/remittances");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
act(() => {
|
||||
result.current?.open("REM-2");
|
||||
});
|
||||
|
||||
expect(pushStateMock).toHaveBeenCalledTimes(1);
|
||||
expect(replaceStateMock).not.toHaveBeenCalled();
|
||||
const urlArg = pushStateMock.mock.calls[0][2] as string;
|
||||
expect(urlArg).toContain("?remit=REM-2");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("setRemitId(id) replaces the current history entry (no new entry) and does NOT pushState", () => {
|
||||
setLocation("http://localhost/remittances?remit=REM-1");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
act(() => {
|
||||
result.current?.setRemitId("REM-3");
|
||||
});
|
||||
|
||||
expect(replaceStateMock).toHaveBeenCalledTimes(1);
|
||||
expect(pushStateMock).not.toHaveBeenCalled();
|
||||
const urlArg = replaceStateMock.mock.calls[0][2] as string;
|
||||
expect(urlArg).toContain("?remit=REM-3");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("open() and close() preserve other query params (only ?remit= is touched)", () => {
|
||||
setLocation("http://localhost/remittances?page=2&remit=REM-1");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
act(() => {
|
||||
result.current?.open("REM-2");
|
||||
});
|
||||
const openUrl = pushStateMock.mock.calls[0][2] as string;
|
||||
expect(openUrl).toContain("page=2");
|
||||
expect(openUrl).toMatch(/[?&]remit=REM-2/);
|
||||
|
||||
act(() => {
|
||||
result.current?.close();
|
||||
});
|
||||
const closeUrl = pushStateMock.mock.calls[1][2] as string;
|
||||
expect(closeUrl).toContain("page=2");
|
||||
expect(closeUrl).not.toContain("remit=");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("close() pushes a new history entry with the ?remit= param stripped", () => {
|
||||
setLocation("http://localhost/remittances?remit=REM-1");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
act(() => {
|
||||
result.current?.close();
|
||||
});
|
||||
|
||||
expect(pushStateMock).toHaveBeenCalledTimes(1);
|
||||
expect(replaceStateMock).not.toHaveBeenCalled();
|
||||
const urlArg = pushStateMock.mock.calls[0][2] as string;
|
||||
expect(urlArg).not.toContain("?remit=");
|
||||
expect(result.current?.remitId).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("updates remitId in response to popstate (browser back/forward)", async () => {
|
||||
setLocation("http://localhost/remittances?remit=REM-1");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
expect(result.current?.remitId).toBe("REM-1");
|
||||
|
||||
setLocation("http://localhost/remittances?remit=REM-2");
|
||||
await act(async () => {
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current?.remitId).toBe("REM-2");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("does not collide with the existing ?claim= param (orthogonal keys)", () => {
|
||||
// The remits drawer and the claims drawer are independent — both
|
||||
// can be open simultaneously in the future, and the hooks must not
|
||||
// stomp each other's URL state. Opening a remit must leave
|
||||
// ?claim= intact, and vice-versa.
|
||||
setLocation("http://localhost/?claim=CLM-1");
|
||||
|
||||
const { result, unmount } = renderHook(() => useRemitDrawerUrlState());
|
||||
|
||||
act(() => {
|
||||
result.current?.open("REM-1");
|
||||
});
|
||||
const openUrl = pushStateMock.mock.calls[0][2] as string;
|
||||
expect(openUrl).toContain("claim=CLM-1");
|
||||
expect(openUrl).toMatch(/[?&]remit=REM-1/);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Read the current `?remit=…` query param off `window.location.search`.
|
||||
* Returns `null` when the param is absent or empty.
|
||||
*
|
||||
* `URLSearchParams` is the standard, locale-free way to parse query
|
||||
* strings in the browser. Using it (rather than hand-rolled string
|
||||
* slicing) means we correctly handle multiple params and percent-encoded
|
||||
* characters in remit IDs without surprises.
|
||||
*
|
||||
* Param name is `?remit=` (not `?remittance=`) — chosen to mirror the
|
||||
* shorter `?claim=` convention used by `useDrawerUrlState` (one-word
|
||||
* token, alphabetical brevity, no collision with the existing
|
||||
* `MatchedRemitCard` which uses the path `/remittances?id={id}`).
|
||||
*/
|
||||
function readRemitId(): string | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const value = params.get("remit");
|
||||
return value === "" ? null : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL we want to push/replace into history.
|
||||
*
|
||||
* - `remitId === null` → drop the `?remit=` param, preserving any
|
||||
* other params (e.g. `?page=2&remit=…` keeps `page=2`).
|
||||
* - `remitId !== null` → set the param to the new id, also preserving
|
||||
* any other params.
|
||||
*
|
||||
* We return `pathname + search + hash` (a relative URL) rather than the
|
||||
* full href — `history.pushState` accepts a relative URL and rewriting
|
||||
* only the relative form keeps the document's origin stable.
|
||||
*/
|
||||
function buildUrl(remitId: string | null): string {
|
||||
const url = new URL(window.location.href);
|
||||
if (remitId === null) {
|
||||
url.searchParams.delete("remit");
|
||||
} else {
|
||||
url.searchParams.set("remit", remitId);
|
||||
}
|
||||
return url.pathname + url.search + url.hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-remittance detail drawer URL state (RemitDrawer).
|
||||
*
|
||||
* Mirrors `useDrawerUrlState` (SP4) but for the remits drawer — reads
|
||||
* `?remit=` from the URL on mount and keeps the value in sync with
|
||||
* history as the drawer is opened, navigated (j/k), and closed.
|
||||
*
|
||||
* - `remitId`: the id parsed from the URL (or `null` when the param
|
||||
* is absent). React state so consumers re-render on changes.
|
||||
* - `open(id)`: pushes a NEW history entry with `?remit={id}` — so the
|
||||
* browser Back button returns to the previous page (e.g. the
|
||||
* remits list) and not just to the previously-open remit.
|
||||
* - `setRemitId(id)`: REPLACES the current history entry — used by the
|
||||
* j/k nav handler so j/k moves through the list without polluting
|
||||
* history with one entry per keystroke.
|
||||
* - `close()`: pushes a NEW entry that strips the param, so Back from
|
||||
* the closed drawer returns to whatever page the user was on before
|
||||
* opening the drawer.
|
||||
*
|
||||
* The hook subscribes to `popstate` so that browser Back/Forward
|
||||
* (which fire popstate rather than our own pushState) propagate into
|
||||
* the React state. Without this, hitting Back would change the URL but
|
||||
* leave the drawer open on the stale id.
|
||||
*/
|
||||
export function useRemitDrawerUrlState(): {
|
||||
remitId: string | null;
|
||||
open: (id: string) => void;
|
||||
close: () => void;
|
||||
setRemitId: (id: string) => void;
|
||||
} {
|
||||
const [remitId, setRemitIdState] = useState<string | null>(() => readRemitId());
|
||||
|
||||
const open = useCallback((id: string) => {
|
||||
window.history.pushState(null, "", buildUrl(id));
|
||||
setRemitIdState(id);
|
||||
}, []);
|
||||
|
||||
const setRemitId = useCallback((id: string) => {
|
||||
window.history.replaceState(null, "", buildUrl(id));
|
||||
setRemitIdState(id);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
window.history.pushState(null, "", buildUrl(null));
|
||||
setRemitIdState(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onPopState = () => {
|
||||
setRemitIdState(readRemitId());
|
||||
};
|
||||
window.addEventListener("popstate", onPopState);
|
||||
return () => window.removeEventListener("popstate", onPopState);
|
||||
}, []);
|
||||
|
||||
return { remitId, open, close, setRemitId };
|
||||
}
|
||||
Reference in New Issue
Block a user