Refactor: Stabilize API function in AuthProvider using useCallback and optimize value memoization
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m45s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m45s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// src/providers/AuthProvider.tsx
|
||||
import React, { useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
import React, { useState, useEffect, useCallback, ReactNode, useMemo } from 'react';
|
||||
import { AuthContext, AuthContextType } from '../contexts/AuthContext';
|
||||
import type { User, UserProfile } from '../types';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
@@ -11,12 +11,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [authStatus, setAuthStatus] = useState<AuthContextType['authStatus']>('Determining...');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { execute: checkTokenApi } = useApi<UserProfile, []>(
|
||||
() => apiClient.getAuthenticatedUserProfile()
|
||||
);
|
||||
const { execute: fetchProfileApi } = useApi<UserProfile, []>(
|
||||
() => apiClient.getAuthenticatedUserProfile()
|
||||
);
|
||||
|
||||
// FIX: Stabilize the apiFunction passed to useApi.
|
||||
// By wrapping this in useCallback, we ensure the same function instance is passed to
|
||||
// useApi on every render. This prevents the `execute` function returned by `useApi`
|
||||
// from being recreated, which in turn breaks the infinite re-render loop in the useEffect below.
|
||||
const getProfileCallback = useCallback(() => apiClient.getAuthenticatedUserProfile(), []);
|
||||
|
||||
const { execute: checkTokenApi } = useApi<UserProfile, []>(getProfileCallback);
|
||||
const { execute: fetchProfileApi } = useApi<UserProfile, []>(getProfileCallback);
|
||||
|
||||
useEffect(() => {
|
||||
// This flag prevents state updates if the component unmounts or if another
|
||||
@@ -29,9 +32,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
if (token) {
|
||||
logger.info('[AuthProvider-Effect] Found auth token. Validating...');
|
||||
try {
|
||||
// Assuming useApi's execute function returns the parsed JSON profile directly.
|
||||
const userProfile = await checkTokenApi();
|
||||
logger.info('[AuthProvider-Effect] Token validation API call complete.', { userProfile });
|
||||
|
||||
|
||||
if (isMounted && userProfile) {
|
||||
logger.info('[AuthProvider-Effect] Profile received, setting state to AUTHENTICATED.');
|
||||
@@ -45,8 +47,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
setProfile(null);
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[AuthProvider-Effect] Error during token validation. Signing out.', { error: e });
|
||||
} catch (e: unknown) {
|
||||
// This catch block is now primarily for unexpected errors, as useApi handles API errors.
|
||||
logger.warn('Auth token validation failed. Clearing token.', { error: e });
|
||||
if (isMounted) {
|
||||
localStorage.removeItem('authToken');
|
||||
setUser(null);
|
||||
@@ -70,7 +73,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
checkAuthToken();
|
||||
|
||||
return () => {
|
||||
logger.info('[AuthProvider-Effect] Component unmounting, cleaning up.');
|
||||
logger.info('[AuthProvider-Effect] Component unmounting, cleaning up.');
|
||||
isMounted = false;
|
||||
};
|
||||
}, [checkTokenApi]);
|
||||
@@ -90,19 +93,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
|
||||
try {
|
||||
const userProfile = await fetchProfileApi();
|
||||
// CRITICAL LOG: What did the API hook actually return?
|
||||
logger.info('[AuthProvider-Login] Profile fetch API call complete.', { userProfile });
|
||||
|
||||
// This is the most likely point of failure if userProfile is not the expected object.
|
||||
if (!userProfile || !userProfile.user) {
|
||||
// If userProfile is not what we expect, it will throw here, get caught, and log the user out.
|
||||
throw new Error('Received invalid profile data from API.');
|
||||
if (!userProfile) {
|
||||
throw new Error('Received null or undefined profile from API.');
|
||||
}
|
||||
|
||||
setUser(userProfile.user);
|
||||
setUser(userProfile!.user);
|
||||
setProfile(userProfile);
|
||||
setAuthStatus('AUTHENTICATED');
|
||||
logger.info('[AuthProvider-Login] Login and profile fetch successful. State set to AUTHENTICATED.');
|
||||
logger.info('[AuthProvider-Login] Login and profile fetch successful. State set to AUTHENTICATED.', { user: loggedInUser });
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
logger.error('Failed to fetch user data after login. Rolling back auth state.', { error: errorMessage });
|
||||
@@ -120,7 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
});
|
||||
}, []);
|
||||
|
||||
const value = { user, profile, authStatus, isLoading, login, logout, updateProfile };
|
||||
const value = useMemo(() => ({ user, profile, authStatus, isLoading, login, logout, updateProfile }), [user, profile, authStatus, isLoading, login, logout, updateProfile]);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
Reference in New Issue
Block a user