Skip to content

fix: stop metadata queries from short-circuiting #10312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 17, 2023
Merged

Conversation

Parkreiner
Copy link
Member

@Parkreiner Parkreiner commented Oct 17, 2023

Doesn't close any open issues, but does address some problems noticed from the company Slack yesterday.

Changes made

  1. Updates all the useQueryOptions factory functions that reference embedded HTML metadata, and makes sure that they can't short-circuit with stale data.
  2. Updates some of the type definitions for getMetadataAsJSON

Other notes

Going to be copy-pasting the thread from the company Slack, just for documentation:

export const appearance = () => {
  return {
    queryKey: ["appearance"],
    queryFn: async () =>
      getMetadataAsJSON<AppearanceConfig>("appearance") ?? API.getAppearance(),
  };
};

export const updateAppearance = (queryClient: QueryClient) => {
  return {
    mutationFn: API.updateAppearance,
    onSuccess: (newConfig: AppearanceConfig) => {
      queryClient.setQueryData(["appearance"], newConfig);
    },
  };
};

The only way for a server request to get made is if the metadata tags in the index.html file aren't set when the server delivers the initial response. Otherwise, the getMetadataAsJSON call will always short-circuit the expression with local data

Another potential bug I'm worried about is background refetches:

  1. updateAppearance gets called, and the onSuccess callback mutates the query client cache with a value different from what was provided via the HTML's meta tag
  2. The user goes to a different tab, and then comes back to Coder
  3. React Query automatically kicks off a background refetch, and calls the query function again
  4. But because the meta tag is still the same from the initial page load, React Query will evaluate the short-circuiting query function and grab the old value from the HTML
  5. The client is stuck with stale data until another mutation gets made, and the stale data spreads to every component subscribed to the query
  6. Even if another mutation does get made, any background refetch would make the data stale again

I'm wondering if it might better to leverage the initialData property to use the metadata tag only while the initial query is loading, but then when we get something back from the server, the initial data will be ignored for all other renders. initialData is never put in the query cache itself, and the only time it could pop up again is if the query cache for that key gets completely vacated.

The getMetadataAsJSON function would need some more massaging to make things have the right types, but I was thinking something like this:

// Defined outside the function to keep renders pure
const initialData = getMetadataAsJSON<AppearanceConfig>("appearance");

export const appearance = () => {
  return {
    initialData,
    queryKey: ["appearance"],
    queryFn: async () => API.getAppearance(),
  } satisfies QueryOptions<AppearanceConfig>;
};

So, uh, famous last words – this is not simple

Basically, React Query has two properties that it lets you define on the query object:

  • placeholderData – This is the property I was actually thinking of. It acts as initial data for the query, but it's never stored in the cache
  • initialData – This works like placeholderData, but it is stored in the cache. By default, this data is always stale, so the query function still runs, even on the first render

Both of these can be actual data or functions that lazy-evaluate to data.

The problem is that the UseQueryOptions type has four type parameter slots:
TQueryFnData – The data we get back from the query function. Defaults to unknown
TError – The error that gets thrown if a query function fails. Defaults to unknown
TData – Used to define the type of initialData (if it's different from TQueryFnData). Defaults to TQueryFnData
TQueryKey – The type of the query key. Defaults to an unknown array

initialData has flexibility – it's defined via TData, and it can default to TQueryFnData if it's not specified. placeholderData doesn't get that privilege, though. It always has to be of type TQueryFnData. And because getMetadataAsJSON can also return undefined, that extra possibility has a risk of "polluting" all the other types.

  1. If we use placeholderData, then the our useQuery object has to be updated to return Data | undefined, meaning the query object's data property will always have an undefined case, even if the query has succeeded
  2. If we use initialData, we have the ability to split hairs and say that the query function returns Data, while the initial data is Data | undefined. But because useQuery's data property is TQueryFnData | TData, we're still polluting the query object

@Parkreiner Parkreiner requested review from a team and aslilac and removed request for a team October 17, 2023 12:45
@@ -6,7 +6,8 @@ import { getMetadataAsJSON } from "utils/metadata";
// so you can just set this to true.
export const experimentalTheme =
typeof document !== "undefined" &&
getMetadataAsJSON("experiments")?.includes("dashboard_theme");
(getMetadataAsJSON<string[]>("experiments")?.includes("dashboard_theme") ??
false);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugly, but had to do this to make sure that the code is always type boolean and not type any

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I hate this lol. but we can fix it later.

@Parkreiner Parkreiner enabled auto-merge (squash) October 17, 2023 12:56
@Parkreiner Parkreiner changed the title fix: prevent metadata queries from short-circuiting fix: stop metadata queries from short-circuiting Oct 17, 2023
@@ -6,7 +6,8 @@ import { getMetadataAsJSON } from "utils/metadata";
// so you can just set this to true.
export const experimentalTheme =
typeof document !== "undefined" &&
getMetadataAsJSON("experiments")?.includes("dashboard_theme");
(getMetadataAsJSON<string[]>("experiments")?.includes("dashboard_theme") ??
false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I hate this lol. but we can fix it later.

@Parkreiner Parkreiner merged commit 0f2d4fd into main Oct 17, 2023
@Parkreiner Parkreiner deleted the mes/json-metadata branch October 17, 2023 16:20
@github-actions github-actions bot locked and limited conversation to collaborators Oct 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants