我在使用 React 本机应用程序时遇到问题,我将状态存储在 Jotai 原子中。
我的问题在于我的组件在应用程序状态更改时更新身份验证状态。 当应用程序从非活动状态变为活动状态时,我收到以下错误:
警告:超出最大更新深度。当 组件在 useEffect 内部调用 setState,但 useEffect 也可以 没有依赖项数组,或者依赖项之一发生更改 每个渲染。
我在 Jotai 库中尝试了不同的策略。在可加载/展开等之间进行更改。 我认为我现在的解决方案效果很好。但是当这个组件运行时,我收到错误。我已将其范围缩小为 Jotai 函数,该函数更新导致循环错误的状态。
这是抛出错误的组件: 它是一个仅在应用程序状态更改时处理状态更新的组件。
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>
)
}
这是应用程序的架构,您可以在其中看到“AuthProvider”在层次结构中的位置。
/**
* 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>
)
}
这是 auth 原子,我在其中声明该原子并从起始 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)
现在的问题是,任何人都可以看到我的一个明显错误,该错误会导致 AuthProvider 的 useEffect 无限循环吗? 我尝试过所有不同的角度。我已经清空了依赖数组,尝试了一些输入、一些输出等。
当与 useEffects 混合并从中设置状态时,Jotai 本身似乎存在一些问题。
真的很期待一些建设性的意见,我现在厌倦了自己尝试。 我真的很喜欢 Jotai 和原子状态的想法,所以我真的不想仅仅因为感觉它应该是一个愚蠢的非问题而切换到其他东西。
刷新验证似乎触发状态更新,导致 useEffect 重新运行。 您可以尝试使用 useCallback 来记住refreshAuth,并将记住的refreshAuth用作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])