From 77ebb89cacac57018c5e634e1dd78596e321abec Mon Sep 17 00:00:00 2001 From: Hemanta Sundaray Date: Sun, 28 Dec 2025 14:19:03 +0530 Subject: [PATCH 1/2] replace useState calls with discriminated union --- src/components/authkit-provider.tsx | 150 +++++++++++++--------------- 1 file changed, 72 insertions(+), 78 deletions(-) diff --git a/src/components/authkit-provider.tsx b/src/components/authkit-provider.tsx index 8d78dd0..89fea91 100644 --- a/src/components/authkit-provider.tsx +++ b/src/components/authkit-provider.tsx @@ -8,20 +8,17 @@ import { refreshAuthAction, switchToOrganizationAction, } from '../actions.js'; -import type { Impersonator, User } from '@workos-inc/node'; +import type { User } from '@workos-inc/node'; import type { UserInfo, SwitchToOrganizationOptions, NoUserInfo } from '../interfaces.js'; -type AuthContextType = { - user: User | null; - sessionId: string | undefined; - organizationId: string | undefined; - role: string | undefined; - roles: string[] | undefined; - permissions: string[] | undefined; - entitlements: string[] | undefined; - featureFlags: string[] | undefined; - impersonator: Impersonator | undefined; - loading: boolean; +type AuthState = + | { + status: 'loading'; + } + | { status: 'unauthenticated'; data: Omit } + | { status: 'authenticated'; data: Omit }; + +type AuthContextType = Omit & { loading: boolean } & { getAuth: (options?: { ensureSignedIn?: boolean }) => Promise; refreshAuth: (options?: { ensureSignedIn?: boolean; organizationId?: string }) => Promise; signOut: (options?: { returnTo?: string }) => Promise; @@ -46,43 +43,63 @@ interface AuthKitProviderProps { initialAuth?: Omit; } +const unauthenticatedState: { status: 'unauthenticated'; data: Omit } = { + status: 'unauthenticated', + data: { + user: null, + sessionId: undefined, + organizationId: undefined, + role: undefined, + roles: undefined, + permissions: undefined, + entitlements: undefined, + featureFlags: undefined, + impersonator: undefined, + }, +}; + +function createAuthState(auth: Omit | undefined): AuthState { + if (!auth) { + return { status: 'loading' }; + } + + if (!auth.user) { + return unauthenticatedState; + } + + return { + status: 'authenticated', + data: { + user: auth.user, + sessionId: auth.sessionId, + organizationId: auth.organizationId, + role: auth.role, + roles: auth.roles, + permissions: auth.permissions, + entitlements: auth.entitlements, + featureFlags: auth.featureFlags, + impersonator: auth.impersonator, + } as Omit, + }; +} + +function getAuthStateData(authState: AuthState): Omit { + if (authState.status === 'loading') { + return unauthenticatedState.data; + } + return authState.data; +} + export const AuthKitProvider = ({ children, onSessionExpired, initialAuth }: AuthKitProviderProps) => { - const [user, setUser] = useState(initialAuth?.user ?? null); - const [sessionId, setSessionId] = useState(initialAuth?.sessionId); - const [organizationId, setOrganizationId] = useState(initialAuth?.organizationId); - const [role, setRole] = useState(initialAuth?.role); - const [roles, setRoles] = useState(initialAuth?.roles); - const [permissions, setPermissions] = useState(initialAuth?.permissions); - const [entitlements, setEntitlements] = useState(initialAuth?.entitlements); - const [featureFlags, setFeatureFlags] = useState(initialAuth?.featureFlags); - const [impersonator, setImpersonator] = useState(initialAuth?.impersonator); - const [loading, setLoading] = useState(!initialAuth); + const [authState, setAuthState] = useState(() => createAuthState(initialAuth)); const getAuth = useCallback(async ({ ensureSignedIn = false }: { ensureSignedIn?: boolean } = {}) => { - setLoading(true); + setAuthState({ status: 'loading' }); try { const auth = await getAuthAction({ ensureSignedIn }); - setUser(auth.user); - setSessionId(auth.sessionId); - setOrganizationId(auth.organizationId); - setRole(auth.role); - setRoles(auth.roles); - setPermissions(auth.permissions); - setEntitlements(auth.entitlements); - setFeatureFlags(auth.featureFlags); - setImpersonator(auth.impersonator); + setAuthState(createAuthState(auth)); } catch (error) { - setUser(null); - setSessionId(undefined); - setOrganizationId(undefined); - setRole(undefined); - setRoles(undefined); - setPermissions(undefined); - setEntitlements(undefined); - setFeatureFlags(undefined); - setImpersonator(undefined); - } finally { - setLoading(false); + setAuthState(unauthenticatedState); } }, []); @@ -105,23 +122,12 @@ export const AuthKitProvider = ({ children, onSessionExpired, initialAuth }: Aut const refreshAuth = useCallback( async ({ ensureSignedIn = false, organizationId }: { ensureSignedIn?: boolean; organizationId?: string } = {}) => { + setAuthState({ status: 'loading' }); try { - setLoading(true); const auth = await refreshAuthAction({ ensureSignedIn, organizationId }); - - setUser(auth.user); - setSessionId(auth.sessionId); - setOrganizationId(auth.organizationId); - setRole(auth.role); - setRoles(auth.roles); - setPermissions(auth.permissions); - setEntitlements(auth.entitlements); - setFeatureFlags(auth.featureFlags); - setImpersonator(auth.impersonator); + setAuthState(createAuthState(auth)); } catch (error) { return error instanceof Error ? { error: error.message } : { error: String(error) }; - } finally { - setLoading(false); } }, [], @@ -184,34 +190,22 @@ export const AuthKitProvider = ({ children, onSessionExpired, initialAuth }: Aut }; }, [onSessionExpired]); - return ( - - {children} - - ); + const contextValue: AuthContextType = { + loading: authState.status === 'loading', + ...getAuthStateData(authState), + getAuth, + refreshAuth, + signOut, + switchToOrganization, + }; + return {children}; }; export function useAuth(options: { ensureSignedIn: true; }): AuthContextType & ({ loading: true; user: User | null } | { loading: false; user: User }); export function useAuth(options?: { ensureSignedIn?: false }): AuthContextType; + export function useAuth({ ensureSignedIn = false }: { ensureSignedIn?: boolean } = {}) { const context = useContext(AuthContext); From 5093a36552805d9ea6ca981a318e1d219d751586 Mon Sep 17 00:00:00 2001 From: Hemanta Sundaray Date: Tue, 6 Jan 2026 17:56:11 +0530 Subject: [PATCH 2/2] refactor: replace useState with useReducer for auth state --- src/components/authkit-provider.tsx | 111 ++++++++++++++++------------ 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/components/authkit-provider.tsx b/src/components/authkit-provider.tsx index 89fea91..048ad5f 100644 --- a/src/components/authkit-provider.tsx +++ b/src/components/authkit-provider.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, ReactNode, useCallback, useContext, useEffect, useReducer } from 'react'; import { checkSessionAction, getAuthAction, @@ -14,11 +14,13 @@ import type { UserInfo, SwitchToOrganizationOptions, NoUserInfo } from '../inter type AuthState = | { status: 'loading'; + data: Omit; } | { status: 'unauthenticated'; data: Omit } | { status: 'authenticated'; data: Omit }; -type AuthContextType = Omit & { loading: boolean } & { +type AuthContextType = Omit & { + loading: boolean; getAuth: (options?: { ensureSignedIn?: boolean }) => Promise; refreshAuth: (options?: { ensureSignedIn?: boolean; organizationId?: string }) => Promise; signOut: (options?: { returnTo?: string }) => Promise; @@ -43,63 +45,73 @@ interface AuthKitProviderProps { initialAuth?: Omit; } -const unauthenticatedState: { status: 'unauthenticated'; data: Omit } = { - status: 'unauthenticated', - data: { - user: null, - sessionId: undefined, - organizationId: undefined, - role: undefined, - roles: undefined, - permissions: undefined, - entitlements: undefined, - featureFlags: undefined, - impersonator: undefined, - }, +const unauthenticatedAuthStateData: Omit = { + user: null, + sessionId: undefined, + organizationId: undefined, + role: undefined, + roles: undefined, + permissions: undefined, + entitlements: undefined, + featureFlags: undefined, + impersonator: undefined, }; -function createAuthState(auth: Omit | undefined): AuthState { - if (!auth) { - return { status: 'loading' }; +function initAuthState(initialAuth: Omit | undefined): AuthState { + if (!initialAuth) { + return { status: 'loading', data: unauthenticatedAuthStateData }; } - if (!auth.user) { - return unauthenticatedState; + if (!initialAuth.user) { + return { status: 'unauthenticated', data: initialAuth as Omit }; } - return { - status: 'authenticated', - data: { - user: auth.user, - sessionId: auth.sessionId, - organizationId: auth.organizationId, - role: auth.role, - roles: auth.roles, - permissions: auth.permissions, - entitlements: auth.entitlements, - featureFlags: auth.featureFlags, - impersonator: auth.impersonator, - } as Omit, - }; + return { status: 'authenticated', data: initialAuth as Omit }; } -function getAuthStateData(authState: AuthState): Omit { - if (authState.status === 'loading') { - return unauthenticatedState.data; +type AuthAction = + | { type: 'START_LOADING' } + | { type: 'SET_AUTH_STATE_AS_UNAUTHENTICATED'; data: Omit } + | { type: 'SET_AUTH_STATE_AS_AUTHENTICATED'; data: Omit } + | { type: 'STOP_LOADING' }; + +function authReducer(state: AuthState, action: AuthAction): AuthState { + switch (action.type) { + case 'START_LOADING': + return { status: 'loading', data: state.data }; + + case 'SET_AUTH_STATE_AS_AUTHENTICATED': + return { status: 'authenticated', data: action.data }; + + case 'SET_AUTH_STATE_AS_UNAUTHENTICATED': + return { status: 'unauthenticated', data: action.data }; + + case 'STOP_LOADING': + if (state.data.user) { + return { status: 'authenticated', data: state.data as Omit }; + } + return { status: 'unauthenticated', data: state.data as Omit }; + + default: + return state; } - return authState.data; } export const AuthKitProvider = ({ children, onSessionExpired, initialAuth }: AuthKitProviderProps) => { - const [authState, setAuthState] = useState(() => createAuthState(initialAuth)); + const [authState, dispatch] = useReducer(authReducer, initialAuth, initAuthState); const getAuth = useCallback(async ({ ensureSignedIn = false }: { ensureSignedIn?: boolean } = {}) => { - setAuthState({ status: 'loading' }); + dispatch({ type: 'START_LOADING' }); try { const auth = await getAuthAction({ ensureSignedIn }); - setAuthState(createAuthState(auth)); + + if (auth.user) { + dispatch({ type: 'SET_AUTH_STATE_AS_AUTHENTICATED', data: auth as Omit }); + } else { + dispatch({ type: 'SET_AUTH_STATE_AS_UNAUTHENTICATED', data: auth as Omit }); + } } catch (error) { - setAuthState(unauthenticatedState); + dispatch({ type: 'SET_AUTH_STATE_AS_UNAUTHENTICATED', data: unauthenticatedAuthStateData }); } }, []); @@ -122,11 +134,17 @@ export const AuthKitProvider = ({ children, onSessionExpired, initialAuth }: Aut const refreshAuth = useCallback( async ({ ensureSignedIn = false, organizationId }: { ensureSignedIn?: boolean; organizationId?: string } = {}) => { - setAuthState({ status: 'loading' }); + dispatch({ type: 'START_LOADING' }); try { const auth = await refreshAuthAction({ ensureSignedIn, organizationId }); - setAuthState(createAuthState(auth)); + + if (auth.user) { + dispatch({ type: 'SET_AUTH_STATE_AS_AUTHENTICATED', data: auth as Omit }); + } else { + dispatch({ type: 'SET_AUTH_STATE_AS_UNAUTHENTICATED', data: auth as Omit }); + } } catch (error) { + dispatch({ type: 'STOP_LOADING' }); return error instanceof Error ? { error: error.message } : { error: String(error) }; } }, @@ -188,16 +206,17 @@ export const AuthKitProvider = ({ children, onSessionExpired, initialAuth }: Aut window.removeEventListener('focus', handleVisibilityChange); window.removeEventListener('visibilitychange', handleVisibilityChange); }; - }, [onSessionExpired]); + }, [onSessionExpired, initialAuth, getAuth]); const contextValue: AuthContextType = { + ...authState.data, loading: authState.status === 'loading', - ...getAuthStateData(authState), getAuth, refreshAuth, signOut, switchToOrganization, }; + return {children}; };