Skip to content
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

feat: upgrade to React Query v5 #1315

Draft
wants to merge 2 commits into
base: ags/react-18
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
118 changes: 21 additions & 97 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"@lukemorales/query-key-factory": "1.3.4",
"@openedx/frontend-slot-footer": "npm:@adamstankiewicz/openedx-frontend-slot-footer@^1.1.3",
"@openedx/paragon": "^22.15.1",
"@tanstack/react-query": "4.28.0",
"@tanstack/react-query-devtools": "4.29.0",
"@tanstack/react-query": "5.66.0",
"@tanstack/react-query-devtools": "5.66.0",
"accessible-nprogress": "2.1.2",
"algoliasearch": "4.6.0",
"classnames": "2.2.6",
Expand Down
83 changes: 41 additions & 42 deletions src/components/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
QueryCache,
QueryClient,
QueryClientProvider,
keepPreviousData,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

Expand All @@ -21,56 +22,54 @@ const ReactQueryDevtoolsProduction = lazy(() => import('@tanstack/react-query-de
default: d.ReactQueryDevtools,
})));

// Create a query client for @tanstack/react-query
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: queryCacheOnErrorHandler,
}),
defaultOptions: {
queries: {
retry: defaultQueryClientRetryHandler,
// Specifying a longer `staleTime` of 20 seconds means queries will not refetch their data
// as often; mitigates making duplicate queries when within the `staleTime` window, instead
// relying on the cached data until the `staleTime` window has exceeded. This may be modified
// per-query, as needed, if certain queries expect to be more up-to-date than others. Allows
// `useQuery` to be used as a state manager.
staleTime: 1000 * 20, // 20 seconds
// By extending `cacheTime`from the default of 5 minutes, we can prevent inactive queries from being garbage
// collected for a longer duration of time. Inactive queries are those that have no rendered query observers
// (e.g., `useQuery` hooks). Since most UI component assumes data will be available and returned by queries
// without having to consider hard loading states, extending the `cacheTime` can help prevent JS errors around
// accessing properties on `undefined` data (due to it being in a hard loading state, `isLoading: true`) by
// delaying when `@tanstack/react-query` garbage collects inactive queries.
cacheTime: 1000 * 60 * 30, // 30 minutes
// To prevent hard loading states if/when query keys change during automatic query background
// re-fetches, we can set `keepPreviousData` to `true` to keep the previous data until the new
// data is fetched. By enabling this option, UI components generally will not need to consider
// explicit loading states when query keys change. Note: `keepPreviousData` is deprecated, replaced
// by `placeholderData` in `@tanstack/react-query` v5 (i.e., for when React is upgraded to v18). See
// https://tanstack.com/query/latest/docs/framework/vue/guides/migrating-to-v5#removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function
// for more details.
keepPreviousData: true,
// Suspense mode on queries enables loading/error states to be caught and handled by a surrounding
// `Suspense` component from React, with a fallback UI component to display while the query is resolving.
// Generally, queries should be resolved within a route loader so it's "guaranteed" to exist within the UI
// components. However, in some cases (e.g., if a query is reset), we attempt to access object properties
// on `undefined` data (i.e., `isLoading: true`) resulting in JS errors. To prevent this error from throwing,
// by enabling suspenseful queries, we can trigger a loading state via a `Suspense` fallback component while
// queries that were removed/reset/garbage collected are re-fetched.
suspense: true,
},
},
});

const App = () => {
const [queryClient] = useState(
() => new QueryClient({
queryCache: new QueryCache({
onError: queryCacheOnErrorHandler,
}),
defaultOptions: {
queries: {
retry: defaultQueryClientRetryHandler,
// Specifying a longer `staleTime` of 20 seconds means queries will not refetch their data
// as often; mitigates making duplicate queries when within the `staleTime` window, instead
// relying on the cached data until the `staleTime` window has exceeded. This may be modified
// per-query, as needed, if certain queries expect to be more up-to-date than others. Allows
// `useQuery` to be used as a state manager.
staleTime: 1000 * 20, // 20 seconds
// By extending `gcTime`from the default of 5 minutes, we can prevent inactive queries from being garbage
// collected for a longer duration of time. Inactive queries are those that have no rendered query observers
// (e.g., `useQuery` hooks). Since most UI components assume data will be available and returned by queries
// without having to consider hard loading states, extending the `gcTime` can help prevent JS errors around
// accessing properties on `undefined` data (due to it being in a hard loading state, `isLoading: true`) by
// delaying when `@tanstack/react-query` garbage collects inactive queries.
gcTime: 1000 * 60 * 30, // 30 minutes
// Suspense mode on queries enables loading/error states to be caught and handled by a surrounding
// `Suspense` component from React, with a fallback UI component to display while the query is resolving.
// Generally, queries should be resolved within a route loader so it's "guaranteed" to exist within the UI
// components. However, in some cases (e.g., if a query is reset), we attempt to access object properties
// on `undefined` data (i.e., `isLoading: true`) resulting in JS errors. To prevent this error from throwing,
// by enabling suspenseful queries, we can trigger a loading state via a `Suspense` fallback component while
// queries that were removed/reset/garbage collected are re-fetched.
suspense: true,
// To prevent hard loading states if/when query keys change during automatic query background
// re-fetches, we can set `keepPreviousData` to `true` to keep the previous data until the new
// data is fetched. By enabling this option, UI components generally will not need to consider
// explicit loading states when query keys change.
placeholderData: keepPreviousData,
},
},
}),
);

const [showReactQueryDevtools, setShowReactQueryDevtools] = useState(false);
useEffect(() => {
window.toggleReactQueryDevtools = () => setShowReactQueryDevtools((prevState) => !prevState);
});

// Create the app router during render vs. at the top-level of the module to ensure
// the logging and auth modules are initialized before the router is created.
const router = useMemo(() => createAppRouter(queryClient), []);
const router = useMemo(() => createAppRouter(queryClient), [queryClient]);

return (
<QueryClientProvider client={queryClient}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/data/hooks/useAcademies.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('useAcademies', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockAcademyListData,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/data/hooks/useAcademyDetails.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('useAcademiesDetails', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockAcademyDetailsData,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/data/hooks/useBFF.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ describe('useBFF', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: expectedData,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/app/data/hooks/useBrowseAndRequest.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('useBrowseAndRequestConfiguration', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockBrowseAndRequestConfiguration,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand All @@ -67,7 +67,7 @@ describe('useSubscriptionLicenseRequests', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockLicenseRequests,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand All @@ -87,7 +87,7 @@ describe('useCouponCodeRequests', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockCouponCodeRequests,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('useCanUpgradeWithLearnerCredit', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: expectedTransformedResult,
isLoading: false,
isPending: false,
}),
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('useContentHighlightSets', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockContentHighlightSets,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand All @@ -88,7 +88,7 @@ describe('useContentHighlightSets', () => {
expect(result.current).toEqual(
expect.objectContaining({
data: mockContentHighlightSets,
isLoading: false,
isPending: false,
isFetching: false,
}),
);
Expand Down
Loading
Loading