Replace queryClient

This commit is contained in:
Eric Bailey
2024-03-21 16:08:43 -05:00
parent 4b881cbb33
commit 8d3375b4db
4 changed files with 140 additions and 64 deletions
+15 -13
View File
@@ -12,7 +12,7 @@ import {
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client' import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted' import {init as initPersistedState} from '#/state/persisted'
@@ -20,11 +20,7 @@ import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler' import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import * as notifications from 'lib/notifications/notifications' import * as notifications from 'lib/notifications/notifications'
import { import {GlobalQueryClientProvider} from 'lib/react-query'
asyncStoragePersister,
dehydrateOptions,
queryClient,
} from 'lib/react-query'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {ThemeProvider} from 'lib/ThemeContext' import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs' import {Provider as DialogStateProvider} from 'state/dialogs'
@@ -55,22 +51,30 @@ import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
function InnerApp() { function InnerApp() {
const queryClient = useQueryClient()
const {isInitialLoad, currentAccount} = useSession() const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const theme = useColorModeTheme() const theme = useColorModeTheme()
const {_} = useLingui() const {_} = useLingui()
useIntentHandler() useIntentHandler()
// init
useEffect(() => { useEffect(() => {
notifications.init(queryClient) notifications.init(queryClient)
listenSessionDropped(() => { }, [queryClient])
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`)) Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
}) })
}, [_])
// init
useEffect(() => {
const account = persisted.get('session').currentAccount const account = persisted.get('session').currentAccount
resumeSession(account) resumeSession(account)
}, [resumeSession, _]) // TODO test
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return ( return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}> <SafeAreaProvider initialMetrics={initialWindowMetrics}>
@@ -121,9 +125,7 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<PersistQueryClientProvider <GlobalQueryClientProvider>
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
<SessionProvider> <SessionProvider>
<ShellStateProvider> <ShellStateProvider>
<PrefsStateProvider> <PrefsStateProvider>
@@ -145,7 +147,7 @@ function App() {
</PrefsStateProvider> </PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
</SessionProvider> </SessionProvider>
</PersistQueryClientProvider> </GlobalQueryClientProvider>
) )
} }
+29 -35
View File
@@ -1,44 +1,38 @@
import 'lib/sentry' // must be near top import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootSiblingParent} from 'react-native-root-siblings'
import 'view/icons' import 'view/icons'
import {ThemeProvider as Alf} from '#/alf' import React, {useEffect, useState} from 'react'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted' import {init as initPersistedState} from '#/state/persisted'
import {Shell} from 'view/shell/index' import * as persisted from '#/state/persisted'
import {ToastContainer} from 'view/com/util/Toast.web'
import {ThemeProvider} from 'lib/ThemeContext'
import {
queryClient,
asyncStoragePersister,
dehydrateOptions,
} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import I18nProvider from './locale/i18nProvider' import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {GlobalQueryClientProvider} from 'lib/react-query'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
useSession, useSession,
useSessionApi, useSessionApi,
} from 'state/session' } from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import {Provider as ShellStateProvider} from 'state/shell'
import * as persisted from '#/state/persisted' import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {ToastContainer} from 'view/com/util/Toast.web'
import {Shell} from 'view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import I18nProvider from './locale/i18nProvider'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
function InnerApp() { function InnerApp() {
const {isInitialLoad, currentAccount} = useSession() const {isInitialLoad, currentAccount} = useSession()
@@ -50,7 +44,9 @@ function InnerApp() {
useEffect(() => { useEffect(() => {
const account = persisted.get('session').currentAccount const account = persisted.get('session').currentAccount
resumeSession(account) resumeSession(account)
}, [resumeSession]) // only run this effect once on first load TODO test
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// wait for session to resume // wait for session to resume
if (isInitialLoad) return null if (isInitialLoad) return null
@@ -100,9 +96,7 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<PersistQueryClientProvider <GlobalQueryClientProvider>
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
<SessionProvider> <SessionProvider>
<ShellStateProvider> <ShellStateProvider>
<PrefsStateProvider> <PrefsStateProvider>
@@ -124,7 +118,7 @@ function App() {
</PrefsStateProvider> </PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
</SessionProvider> </SessionProvider>
</PersistQueryClientProvider> </GlobalQueryClientProvider>
) )
} }
@@ -1,9 +1,14 @@
import React from 'react'
import {AppState, AppStateStatus} from 'react-native' import {AppState, AppStateStatus} from 'react-native'
import {QueryClient, focusManager} from '@tanstack/react-query'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import {PersistQueryClientProviderProps} from '@tanstack/react-query-persist-client' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import {focusManager, QueryClient} from '@tanstack/react-query'
import {
PersistQueryClientProvider,
PersistQueryClientProviderProps,
} from '@tanstack/react-query-persist-client'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
// any query keys in this array will be persisted to AsyncStorage // any query keys in this array will be persisted to AsyncStorage
@@ -35,6 +40,19 @@ focusManager.setEventListener(onFocus => {
} }
}) })
export const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'queryCache',
})
export const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
return STORED_CACHE_QUERY_KEYS.includes(String(query.queryKey[0]))
},
}
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
@@ -56,15 +74,73 @@ export const queryClient = new QueryClient({
}, },
}) })
export const asyncStoragePersister = createAsyncStoragePersister({ function createGlobalQueryClient() {
storage: AsyncStorage, return new QueryClient({
key: 'queryCache', defaultOptions: {
queries: {
// NOTE
// refetchOnWindowFocus breaks some UIs (like feeds)
// so we only selectively want to enable this
// -prf
refetchOnWindowFocus: false,
// Structural sharing between responses makes it impossible to rely on
// "first seen" timestamps on objects to determine if they're fresh.
// Disable this optimization so that we can rely on "first seen" timestamps.
structuralSharing: false,
// We don't want to retry queries by default, because in most cases we
// want to fail early and show a response to the user. There are
// exceptions, and those can be made on a per-query basis. For others, we
// should give users controls to retry.
retry: false,
},
},
})
}
let __globalQueryClient = createGlobalQueryClient()
export function getGlobalQueryClient() {
return __globalQueryClient
}
type Context = {
resetGlobalQueryClient(): void
}
const Context = React.createContext<Context>({
resetGlobalQueryClient() {},
}) })
export const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = export function useResetGlobalQueryClient() {
{ return React.useContext(Context).resetGlobalQueryClient
shouldDehydrateMutation: (_: any) => false, }
shouldDehydrateQuery: query => {
return STORED_CACHE_QUERY_KEYS.includes(String(query.queryKey[0])) export function GlobalQueryClientProvider({
children,
}: {
children: React.ReactNode
}) {
const [queryClient, setQueryClient] = React.useState(() =>
createGlobalQueryClient(),
)
const context = React.useMemo<Context>(() => {
return {
resetGlobalQueryClient() {
logger.debug(`react query: setting new global query client`)
const client = createGlobalQueryClient()
__globalQueryClient = client
setQueryClient(client)
}, },
} }
}, [])
return (
<Context.Provider value={context}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
{children}
</PersistQueryClientProvider>
</Context.Provider>
)
}
+7 -3
View File
@@ -10,6 +10,7 @@ import {jwtDecode} from 'jwt-decode'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
import {IS_TEST_USER} from '#/lib/constants' import {IS_TEST_USER} from '#/lib/constants'
import {useResetGlobalQueryClient} from '#/lib/react-query'
import {logEvent, LogEvents} from '#/lib/statsig/statsig' import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {hasProp} from '#/lib/type-guards' import {hasProp} from '#/lib/type-guards'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -179,6 +180,7 @@ function createPersistSessionHandler(
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const resetGlobalQueryClient = useResetGlobalQueryClient()
const isDirty = React.useRef(false) const isDirty = React.useRef(false)
const [state, setState] = React.useState<SessionState>({ const [state, setState] = React.useState<SessionState>({
isInitialLoad: true, isInitialLoad: true,
@@ -373,6 +375,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
persistSession: createPersistSessionHandler( persistSession: createPersistSessionHandler(
account, account,
({expired, refreshedAccount}) => { ({expired, refreshedAccount}) => {
console.log('RESUME: PERSIST')
upsertAccount(refreshedAccount, expired) upsertAccount(refreshedAccount, expired)
}, },
{networkErrorCallback: clearCurrentAccount}, {networkErrorCallback: clearCurrentAccount},
@@ -414,8 +417,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
* state so that the app can rerender with new data. * state so that the app can rerender with new data.
*/ */
agent.session = prevSession agent.session = prevSession
resetGlobalQueryClient()
__globalAgent = agent __globalAgent = agent
queryClient.clear()
upsertAccount(account) upsertAccount(account)
if (prevSession.deactivated) { if (prevSession.deactivated) {
@@ -444,6 +447,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
try { try {
// this calls `upsertAccount` via `agent.resumeSession` and persistor // this calls `upsertAccount` via `agent.resumeSession` and persistor
await resumeSessionWithFreshAccount() await resumeSessionWithFreshAccount()
console.log('RESUME: DONE')
resetGlobalQueryClient()
// only update global agen if resumeSessionWithFreshAccount succeeded // only update global agen if resumeSessionWithFreshAccount succeeded
__globalAgent = agent __globalAgent = agent
} catch (e) { } catch (e) {
@@ -456,7 +461,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}) })
__globalAgent = PUBLIC_BSKY_AGENT __globalAgent = PUBLIC_BSKY_AGENT
} finally {
queryClient.clear() queryClient.clear()
} }
} }
@@ -487,7 +491,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
} }
}, },
[upsertAccount, queryClient, clearCurrentAccount], [upsertAccount, queryClient, clearCurrentAccount, resetGlobalQueryClient],
) )
const resumeSession = React.useCallback<ApiContext['resumeSession']>( const resumeSession = React.useCallback<ApiContext['resumeSession']>(