Skip to content

Commit

Permalink
chore(nodejs): upgrade to React Query v5
Browse files Browse the repository at this point in the history
This version has some fairly major changes, but most of it is hidden
from us thanks to Orval. We have made the following notable changes,
however:

* `cacheTime` has been renamed to `gcTime`. As far as I can tell,
there are no behavioral changes and this is simply a rename to reduce
confusion. Therefore, this change should be purely cosmetic. (See
TanStack/query#4678,
TanStack/query#1217, and
TanStack/query#4829 for reference.)

* Use object-style parameters to query methods where necessary. These
changes should be purely cosmetic.

* React Query v5 is written in TypeScript, so we can take advantage of
better types. The one place we currently do that is in the `Edit`
component's `useGetProgram` return types. Note that we also use
`isPending` rather than `isLoading`. The docs are a bit confusing on
this point as to whether they're equivalent; see
https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#status-loading-has-been-changed-to-status-pending-and-isloading-has-been-changed-to-ispending-and-isinitialloading-has-now-been-renamed-to-isloading.
Initially I used `isLoading` when porting to v5, but TypeScript was
not able to narrow the `data` value to non-nil unless I use
`isPending`, so I've gone with `isPending` in the end. As far as I can
tell, it works the same as before.

* v5's dev tools are significantly improved, including the UI, which
lets you choose via a nice drop-down menu where to locate the tool
pop-up. This doesn't play well with our own "dev tools" checkbox UI,
so I've temporarily disabled those `DevOptions`, as the UI for them
will need to be rethought. I think adding another button to the canvas
is probably the way to go, but I'll leave that work for later.

FYI, the migration guide is here, and it informed most of these
changes:

https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5

Signed-off-by: Drew Hess <[email protected]>
  • Loading branch information
dhess committed Apr 10, 2024
1 parent dba7796 commit 3726e3f
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 186 deletions.
4 changes: 2 additions & 2 deletions orval.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const useQueryPost: {
})
);

// We disable caching by default, to avoid displaying stale data.
const queryOpts: UseQueryOptions = { cacheTime: 0 };
// gc immediately, to disable showing cached data.
const queryOpts: UseQueryOptions = { gcTime: 0 };

export default defineConfig({
"primer-api": {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"@hookform/resolvers": "^3.3.4",
"@neodrag/react": "^2.0.4",
"@orval/core": "^6.26.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
"@tanstack/react-query": "^5.29.0",
"@tanstack/react-query-devtools": "^5.29.0",
"@types/deep-equal": "^1.0.4",
"@zxch3n/tidy": "github:hackworthltd/tidy#e07fdef2ae7bf593701817113dd47b4cd56c7a97",
"axios": "^1.6.8",
Expand Down
75 changes: 18 additions & 57 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 12 additions & 82 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { CookiesProvider, useCookies } from "react-cookie";
import { CookieSetOptions } from "universal-cookie";
import { v4 as uuidv4 } from "uuid";
import { WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
import { Resizable } from "re-resizable";

import "@/index.css";

import { ChooseSession, Edit, NoMatch } from "@/components";
import { DevOptions } from "@/components/Edit";

// This ensures that we don't unnecessarily load the tools in production.
// https://tanstack.com/query/v4/docs/react/devtools#devtools-in-production
const ReactQueryDevtoolsPanel = lazy(() =>
import("@tanstack/react-query-devtools/build/lib/index.prod.js").then(
//
// Ref:
// https://tanstack.com/query/latest/docs/framework/react/devtools#devtools-in-production
const ReactQueryDevtools = lazy(() =>
import("@tanstack/react-query-devtools/build/modern/production.js").then(
(d) => ({
default: d.ReactQueryDevtoolsPanel,
default: d.ReactQueryDevtools,
})
)
);
Expand All @@ -37,16 +36,7 @@ const idCookieOptions = (path: string): CookieSetOptions => {

const App = (): JSX.Element => {
const [cookies, setCookie] = useCookies(["id"]);
const [enableDevtools, setEnableDevtools] = useState(import.meta.env.DEV);
const [devtoolsOpen, setDevtoolsOpen] = useState(false);

const devToolsMinHeight = 250;
const devToolsMaxHeight = 500;
const [devOpts, setDevOpts] = useState<DevOptions>({
showIDs: false,
inlineLabels: false,
alwaysShowLabels: true,
});
const [showDevtools, setShowDevtools] = useState(import.meta.env.DEV);

useEffect(() => {
if (!cookies.id) {
Expand All @@ -60,47 +50,24 @@ const App = (): JSX.Element => {

useEffect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.toggleDevtools =
// This comment forces a line break to limit the scope of `@ts-ignore`.
() => setEnableDevtools((old) => !old);
// @ts-expect-error
window.toggleDevtools = () => setShowDevtools((old) => !old);
}, []);

return (
<CookiesProvider>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
{enableDevtools && (
{showDevtools && (
<Suspense fallback={null}>
<button
className="absolute right-0 z-50 p-4"
onClick={() => setDevtoolsOpen((old) => !old)}
>
<WrenchScrewdriverIcon className="h-10 fill-grey-primary"></WrenchScrewdriverIcon>
</button>
{devtoolsOpen && (
<Resizable
enable={{ bottom: true }}
defaultSize={{ height: devToolsMinHeight, width: "100%" }}
className="fixed grid grid-cols-[minmax(0,2fr)_1fr]"
minHeight={devToolsMinHeight}
maxHeight={devToolsMaxHeight}
>
<ReactQueryDevtoolsPanel
style={{ height: "inherit", maxHeight: devToolsMaxHeight }}
setIsOpen={setDevtoolsOpen}
onDragStart={(_) => {}}
/>
<DevMenu opts={devOpts} set={setDevOpts} />
</Resizable>
)}
<ReactQueryDevtools position="top" buttonPosition="top-right" />
</Suspense>
)}
<Routes>
<Route path="/" element={<Navigate to="/sessions" />} />
<Route path="/sessions">
<Route index element={<ChooseSession />} />
<Route path=":sessionId" element={<Edit {...devOpts} />} />
<Route path=":sessionId" element={<Edit />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
Expand All @@ -110,41 +77,4 @@ const App = (): JSX.Element => {
);
};

const DevMenu = (p: { opts: DevOptions; set: (opts: DevOptions) => void }) => (
<div className="bg-blue-primary pl-1 text-white-primary">
<div>
<input
type="checkbox"
id="showIDs"
checked={p.opts.showIDs}
onChange={(e) => p.set({ ...p.opts, showIDs: e.target.checked })}
className="mr-1"
/>
<label htmlFor="showIDs">show node IDs</label>
</div>
<div>
<input
type="checkbox"
id="alwaysShowLabels"
checked={p.opts.alwaysShowLabels}
onChange={(e) =>
p.set({ ...p.opts, alwaysShowLabels: e.target.checked })
}
className="mr-1"
/>
<label htmlFor="alwaysShowLabels">always show labels</label>
</div>
<div>
<input
type="checkbox"
id="inlineLabels"
checked={p.opts.inlineLabels}
onChange={(e) => p.set({ ...p.opts, inlineLabels: e.target.checked })}
className="mr-1"
/>
<label htmlFor="inlineLabels">inline labels</label>
</div>
</div>
);

export default App;
8 changes: 6 additions & 2 deletions src/components/ChooseSession/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const ChooseSession = (): JSX.Element => {
const deleteSession = useDeleteSession({
mutation: {
onSuccess: () =>
queryClient.invalidateQueries(getGetSessionListQueryKey()),
queryClient.invalidateQueries({
queryKey: getGetSessionListQueryKey(),
}),
},
});
const { data } = useGetSessionList({
Expand Down Expand Up @@ -57,7 +59,9 @@ const ChooseSession = (): JSX.Element => {
const newSession = useCreateSession({
mutation: {
onSuccess: (newSessionID: Uuid) => {
queryClient.invalidateQueries(getGetSessionListQueryKey());
queryClient.invalidateQueries({
queryKey: getGetSessionListQueryKey(),
});
navigate(`/sessions/${newSessionID}`);
},
},
Expand Down
51 changes: 26 additions & 25 deletions src/components/Edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,17 @@ export type DevOptions = {
alwaysShowLabels: boolean;
};

const Edit = (devOpts: DevOptions): JSX.Element => {
const Edit = (): JSX.Element => {
const params = useParams();
const sessionId = params["sessionId"];

// Temporary until these toggles are re-enabled.
const devOpts: DevOptions = {
showIDs: false,
inlineLabels: false,
alwaysShowLabels: false,
};

if (!sessionId) {
return (
<Error string={"No sessionId parameter: " + JSON.stringify(params)} />
Expand All @@ -74,33 +81,24 @@ const Edit = (devOpts: DevOptions): JSX.Element => {
// This hook is *technically* conditional.
// But if the condition above fails, then the app is broken anyway.
// eslint-disable-next-line react-hooks/rules-of-hooks
const queryRes = useGetProgram(sessionId);
const { isPending, isError, data, error } = useGetProgram(sessionId);

if (queryRes.error) {
if (isError) {
return (
<Error
string={
"Failed to get program from backend: " +
JSON.stringify(queryRes.error)
}
/>
<Error string={"Failed to get program from backend: " + error.message} />
);
}

// This state will appear on every load, usually only very briefly,
// and we choose to just show nothing.
if (queryRes.isLoading) {
} else if (isPending) {
// This state will appear on every load, usually only very briefly,
// and we choose to just show nothing.
return <></>;
}

// At this point, we have successfully received an initial program.
return (
<AppProg
initialProg={queryRes.data}
{...{ sessionId }}
devOpts={devOpts}
></AppProg>
);
} else
return (
<AppProg
initialProg={data}
{...{ sessionId }}
devOpts={devOpts}
></AppProg>
);
};

const AppProg = (p: {
Expand Down Expand Up @@ -190,7 +188,10 @@ const useInvalidateOnChange = <TData, TError>(
useEffect(
() => {
(async () =>
await queryClient.invalidateQueries(res.queryKey, { exact: true }))();
await queryClient.invalidateQueries({
queryKey: res.queryKey,
exact: true,
}))();
},
// We stringify the queryKey as a poor-man's deep equality check,
// since the orval generated bindings to react-query construct a
Expand Down
Loading

0 comments on commit 3726e3f

Please sign in to comment.