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

This commit is contained in:
2025-12-16 01:41:40 -08:00
parent f34c66bab1
commit 2cec27c03b
2 changed files with 22 additions and 23 deletions

View File

@@ -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>;
};