11Korvar
11Korvar

Reputation: 21

Jotai useEffect Maximum update depth exceeded

I'm having an issue with a React native app where i store state in Jotai atoms.

My issue lies in my component that updates auth state on app state change. When the app goes from inactive -> active, i get these errors:

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

I've tried different tactics with the Jotai library. Changing between loadable/unwrap etc. The solution i have now works fine in my opinion. But when this component runs, i get the errors. And i've narrowed it down to being the Jotai function that updates state that causes the loop error.

This is the component that throws the errors: It's a component that only handles the updating of state when the app's state changes.

import { refreshAuthAtom } from '@state/auth/auth.atom'
import { useSetAtom } from 'jotai'
import React, { createContext, useContext, useEffect, useRef } from 'react'
import { AppState } from 'react-native'
import { useLogin } from './use-login.hook'
type AuthProviderProps = {
  children?: React.ReactNode
}

const PasswordAuthContext = createContext({})

/**
 *
 * @description This hook is used to get the auth context.
 * @returns {PasswordAuthContextProps}
 */
export const useAuth = () => useContext(PasswordAuthContext)

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const refreshAuth = useSetAtom(refreshAuthAtom)
  const { logout } = useLogin()
  const appState = useRef(AppState.currentState)

  useEffect(() => {
    const subscription = AppState.addEventListener(
      'change',
      async (nextState) => {
        const previousStateIsBackgroundOrInactive =
          appState.current.match(/inactive|background/)

        if (previousStateIsBackgroundOrInactive && nextState === 'active') {
          try {
            await refreshAuth() //If i remove this function, i get no errors.
            //So something about setting state here, causes the "Maximum update 
            //depth"-error.
            console.log('AUTH REFRESHED')
          } catch (e) {
            console.log('error refreshing auth: ', e)
            logout()
          }
        }
        //save appstate to ref
        //so that we can compare it in the next iteration
        appState.current = nextState
      }
    )

    return () => {
      subscription.remove()
    }
  }, [refreshAuth, logout])

  return (
    <PasswordAuthContext.Provider value={{}}>
      {children}
    </PasswordAuthContext.Provider>
  )
}

This is the architecture of the app, where you can see the "AuthProvider"'s place in the hierarchy.

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 */
import React, { Suspense, useEffect } from 'react'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

import { ThemeProvider } from 'styled-components/native'
import { darkTheme, defaultTheme, useCustomTheme } from './src/ui/core/theme'

import { NavigationContainer } from '@react-navigation/native'
import SplashScreen from 'react-native-splash-screen'
import { AuthProvider } from './src/core/auth/auth.provider'
import { RootFlashMessageProvider } from './src/core/notifications/flash-message.provider'
import { NotificationProvider } from './src/core/push-notifications/notification-provider'
import { UserProvider } from './src/core/user/user.provider'
import { InitProvider } from './src/features/Init/init.provider'
import { RootStack } from './src/navigation'
import { linking } from './src/navigation/deep-linking/linking'
import { CenteredActivityIndicator } from './src/ui/components/loader/centered-activity-indicator'
import { ScrollProvider } from './src/ui/components/scroll-view/scroll.provider'

export default function App() {
  const { isDarkMode } = useCustomTheme()
  const queryClient = new QueryClient()
  const theme = isDarkMode ? darkTheme : defaultTheme

  useEffect(() => {
    SplashScreen.hide()
  }, [])

  return (
    <NavigationContainer linking={linking}>
      <QueryClientProvider client={queryClient}>
        <ThemeProvider theme={theme}>
          <NotificationProvider>
            <Suspense fallback={<CenteredActivityIndicator />}>
              <AuthProvider />
              <UserProvider />
              <InitProvider />
              <ScrollProvider>
                <RootFlashMessageProvider>
                  <RootStack />
                </RootFlashMessageProvider>
              </ScrollProvider>
            </Suspense>
          </NotificationProvider>
        </ThemeProvider>
      </QueryClientProvider>
    </NavigationContainer>
  )
}

Here's the auth atom, where i declare the atom aswell as create some others from the starting authStore:

import AsyncStorage from '@react-native-async-storage/async-storage'
import { atom } from 'jotai'
import { atomWithStorage, createJSONStorage, loadable } from 'jotai/utils'
import { refresh } from 'react-native-app-auth'
import { SESAMY_CONFIG } from '../../core/auth/config'

export interface AuthStore {
  accessToken: string
  refreshToken: string
  expiresAt: string
  idToken: string
}

export const initAuthStore = {
  accessToken: '',
  refreshToken: '',
  expiresAt: '',
  idToken: '',
}

export const authAtom = atomWithStorage<AuthStore>(
  'authStore',
  initAuthStore,
  createJSONStorage(() => AsyncStorage),
  { getOnInit: true }
)

export const refreshAuthAtom = atom(null, async (get, set) => {
  const auth = await get(authAtom)
  const refreshedAuth = await refresh(SESAMY_CONFIG, {
    refreshToken: auth.refreshToken,
  })
  if (!refreshedAuth.accessToken || !refreshedAuth.refreshToken) {
    throw new Error('No access token or refresh token found')
  }
  set(authAtom, {
    accessToken: refreshedAuth.accessToken,
    refreshToken: refreshedAuth.refreshToken,
    expiresAt: refreshedAuth.accessTokenExpirationDate,
    idToken: refreshedAuth.idToken,
  })
})

const asyncAccessTokenAtom = atom(
  async (get) => (await get(authAtom)).accessToken
)
// export const accessTokenAtom = unwrap(asyncAccessTokenAtom)
export const loadableAccessToken = loadable(asyncAccessTokenAtom)

const asyncIdTokenAtom = atom(async (get) => (await get(authAtom)).idToken)
export const idTokenAtom = loadable(asyncIdTokenAtom)

const asyncIsAuthenticatedAtom = atom(
  async (get) => (await get(asyncAccessTokenAtom))?.length > 0
)
export const isAuthenticatedAtom = loadable(asyncIsAuthenticatedAtom)

Now the question is, can anyone see a clear error of mine that would cause infinite loops in the AuthProvider's useEffect? I've tried all different angles. I've emptied the dependency-array, tried with some in, some out etc.

It almost seems like there is some issue with Jotai itself when mixed with useEffects and setting state from it.

Really looking forward to some constructive opinions, i'm tired of trying by myself now. I really love the idea of Jotai and atomic state, so i really don't want to have to switch to something else just because of something that feels like it should be an idiotic non-issue.

Upvotes: 0

Views: 323

Answers (1)

Praneeth Welideniya
Praneeth Welideniya

Reputation: 76

Seems refreshAuth triggering state updates that cause the useEffect to re-run. You can try to memorise the refreshAuth using useCallback , use memorised refreshAuth as a dependent of useEffect.

const memoizedRefreshAuth = useCallback(refreshAuth, []);

useEffect(() => {
  const subscription = AppState.addEventListener(
    'change',
    async (nextState) => {
      const previousStateIsBackgroundOrInactive =
        appState.current.match(/inactive|background/)

      if (previousStateIsBackgroundOrInactive && nextState === 'active') {
        try {
          await memoizedRefreshAuth() // Using memoized function here
          console.log('AUTH REFRESHED')
        } catch (e) {
          console.log('error refreshing auth: ', e)
          logout()
        }
      }
      //save appstate to ref
      //so that we can compare it in the next iteration
      appState.current = nextState
    }
  )
//Other codes
}, [memoizedRefreshAuth, logout])

Upvotes: 0

Related Questions