Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions web/src/components/core/Page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
<div>ProductRegistrationAlertMock</div>
));

jest.mock("~/components/core/ProgressBackdrop", () => () => <div>ProgressBackdropMock</div>);

const mockUseTrackQueriesRefetch = jest.mocked(useTrackQueriesRefetch);

describe("Page", () => {
Expand Down Expand Up @@ -74,6 +76,20 @@ describe("Page", () => {
screen.getByRole("heading", { name: "The Page Component" });
});

describe("when no progress prop is provided", () => {
it("does not mount ProgressBackdrop", () => {
installerRender(<Page />);
expect(screen.queryByText("ProgressBackdropMock")).toBeNull();
});
});

describe("when progress prop is provided", () => {
it("mounts ProgressBackdrop", () => {
installerRender(<Page progress={{ scope: "software" }} />);
screen.getByText("ProgressBackdropMock");
});
});

describe("Page.Actions", () => {
it("renders a footer sticky to bottom", () => {
installerRender(
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ const Page = ({
return (
<PageGroup {...pageGroupProps} tabIndex={-1} id="main-content">
{children}
<ProgressBackdrop {...progress} />
{progress && <ProgressBackdrop {...progress} />}
</PageGroup>
);
};
Expand Down
7 changes: 0 additions & 7 deletions web/src/components/core/ProgressBackdrop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,6 @@ describe("ProgressBackdrop", () => {
jest.clearAllMocks();
});

describe("when no progress scope is provided", () => {
it("does not render the backdrop", () => {
installerRender(<ProgressBackdrop />);
expect(screen.queryByRole("alert")).toBeNull();
});
});

describe("when progress scope is provided but no matching progress exists", () => {
it("does not render the backdrop", () => {
installerRender(<ProgressBackdrop scope="software" />);
Expand Down
54 changes: 51 additions & 3 deletions web/src/context/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import React from "react";
import { InstallerClientProvider } from "./installer";
import { InstallerL10nProvider } from "./installerL10n";
import { StorageUiStateProvider } from "./storage-ui-state";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
DefaultOptions,
MutationOptions,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { localConnection } from "~/utils";

// Determines which "network mode" should Tanstack Query use
Expand All @@ -40,13 +45,56 @@ const networkMode = (): "always" | "online" => {
return localConnection() ? "always" : "online";
};

const sharedOptions = {
const sharedOptions: DefaultOptions & MutationOptions = {
networkMode: networkMode(),
};

const queryClient = new QueryClient({
defaultOptions: {
queries: sharedOptions,
queries: {
...sharedOptions,
/**
* Structural sharing is disabled to ensure QueryCache subscriptions
* receive 'updated' events even when refetched data is identical to
* previous data.
*
* With structural sharing enabled (default), TanStack Query reuses the
* previous data reference when new data is deeply equal, preventing the
* QueryCache from emitting update events. This makes it impossible to
* detect refetches via subscriptions when data hasn't changed.
*
* The custom useTrackQueriesRefetch hook (used by ProgressBackdrop)
* relies on these events to detect when queries have been refetched,
* enabling it to unblock the UI at the right moment when a progress has
* finished and data for rendering the interface is ready. Without these
* events, the UI cannot reliably determine when to unblock.
*
* The performance impact of disabling this optimization is expected to be
* minimal because:
*
* * Identical data responses are relatively rare in practice
* * React's reconciliation efficiently skips DOM updates when rendered
* output is identical, even if components re-render. Any heavy
* computations can be memoized to avoid re-execution when data hasn't
* changed
*
* Several alternatives were evaluated and rejected as unnecessarily complex
* for the value provided:
*
* * Adding artificial timestamps to JSON responses (which achieves
* nearly the same result as disabling this optimization, but at the
* cost of breaking types and polluting the data model)
* * Reverting useTrackQueriesRefetch to query observer pattern
* * Manually configuring notifyOnChangeProps globally or per-query
* * Implementing separate tracker queries alongside data queries
*
* This approach simply trades one render optimization for simpler, more
* reliable event detection.
*
* @see https://tanstack.com/query/v5/docs/framework/react/guides/render-optimizations
*/
structuralSharing: false,
},
mutations: sharedOptions,
},
});
Expand Down
Loading