Files
fast-next-template/frontend/docs/COMMON_PITFALLS.md
Felipe Cardoso 96df7edf88 Refactor useAuth hook, settings components, and docs for formatting and readability improvements
- Consolidated multi-line arguments into single lines where appropriate in `useAuth`.
- Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`).
- Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`).
- Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
2025-11-10 11:03:45 +01:00

20 KiB

Frontend Common Pitfalls & Solutions

Project: Next.js + FastAPI Template Version: 1.0 Last Updated: 2025-11-03 Status: Living Document


Table of Contents

  1. React Hooks
  2. Context API & State Management
  3. Zustand Store Patterns
  4. TypeScript Type Safety
  5. Component Patterns
  6. Provider Architecture
  7. Event Handlers & Callbacks
  8. Testing Pitfalls
  9. Performance
  10. Import/Export Patterns

1. React Hooks

Pitfall 1.1: Returning Hook Function Instead of Calling It

WRONG:

// Custom hook that wraps Zustand
export function useAuth() {
  const storeHook = useContext(AuthContext);
  return storeHook; // Returns the hook function itself!
}

// Consumer component
function MyComponent() {
  const authHook = useAuth(); // Got the hook function
  const { user } = authHook(); // Have to call it here ❌ Rules of Hooks violation!
}

Why It's Wrong:

  • Violates React Rules of Hooks (hook called conditionally/in wrong place)
  • Confusing API for consumers
  • Can't use in conditionals or callbacks safely
  • Type inference breaks

CORRECT:

// Custom hook that calls the wrapped hook internally
export function useAuth() {
  const storeHook = useContext(AuthContext);
  if (!storeHook) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return storeHook(); // Call the hook HERE, return the state
}

// Consumer component
function MyComponent() {
  const { user } = useAuth(); // Direct access to state ✅
}

EVEN BETTER (Polymorphic):

// Support both patterns
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
  const storeHook = useContext(AuthContext);
  if (!storeHook) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return selector ? storeHook(selector) : storeHook();
}

// Usage - both work!
const { user } = useAuth(); // Full state
const user = useAuth((s) => s.user); // Optimized selector

Key Takeaway:

  • Always call hooks internally in custom hooks
  • Return state/values, not hook functions
  • Support selectors for performance optimization

Pitfall 1.2: Calling Hooks Conditionally

WRONG:

function MyComponent({ showUser }) {
  if (showUser) {
    const { user } = useAuth();  // ❌ Conditional hook call!
    return <div>{user?.name}</div>;
  }
  return null;
}

CORRECT:

function MyComponent({ showUser }) {
  const { user } = useAuth();  // ✅ Always call at top level

  if (!showUser) {
    return null;
  }

  return <div>{user?.name}</div>;
}

Key Takeaway:

  • Always call hooks at the top level of your component
  • Never call hooks inside conditionals, loops, or nested functions
  • Return early after hooks are called

2. Context API & State Management

Pitfall 2.1: Creating New Context Value on Every Render

WRONG:

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  // New object created every render! ❌
  const value = { user, setUser };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Why It's Wrong:

  • Every render creates a new object
  • All consumers re-render even if values unchanged
  • Performance nightmare in large apps

CORRECT:

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  // Memoize value - only changes when dependencies change
  const value = useMemo(() => ({ user, setUser }), [user]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

EVEN BETTER (Zustand + Context):

export function AuthProvider({ children, store }) {
  // Zustand hook function is stable (doesn't change)
  const authStore = store ?? useAuthStoreImpl;

  // No useMemo needed - hook functions are stable references
  return <AuthContext.Provider value={authStore}>{children}</AuthContext.Provider>;
}

Key Takeaway:

  • Use useMemo for Context values that are objects
  • Or use stable references (Zustand hooks, refs)
  • Monitor re-renders with React DevTools

Pitfall 2.2: Prop Drilling Instead of Context

WRONG:

// Passing through 5 levels
<Layout user={user}>
  <Sidebar user={user}>
    <Navigation user={user}>
      <UserMenu user={user}>
        <Avatar user={user} />
      </UserMenu>
    </Navigation>
  </Sidebar>
</Layout>

CORRECT:

// Provider at top
<AuthProvider>
  <Layout>
    <Sidebar>
      <Navigation>
        <UserMenu>
          <Avatar />  {/* Gets user from useAuth() */}
        </UserMenu>
      </Navigation>
    </Sidebar>
  </Layout>
</AuthProvider>

Key Takeaway:

  • Use Context for data needed by many components
  • Avoid prop drilling beyond 2-3 levels
  • But don't overuse - local state is often better

3. Zustand Store Patterns

Pitfall 3.1: Mixing Render State Access and Mutation Logic

WRONG (Mixing patterns):

function MyComponent() {
  // Using hook for render state
  const { user } = useAuthStore();

  const handleLogin = async (data) => {
    // Also using hook in callback ❌ Inconsistent!
    const setAuth = useAuthStore((s) => s.setAuth);
    await setAuth(data.user, data.token);
  };
}

CORRECT (Separate patterns):

function MyComponent() {
  // Hook for render state (subscribes to changes)
  const { user } = useAuthStore();

  const handleLogin = async (data) => {
    // getState() for mutations (no subscription)
    const setAuth = useAuthStore.getState().setAuth;
    await setAuth(data.user, data.token);
  };
}

Why This Pattern?

  • Render state: Use hook → component re-renders on changes
  • Mutations: Use getState() → no subscription, no re-renders
  • Performance: Event handlers don't need to subscribe
  • Clarity: Clear distinction between read and write

Key Takeaway:

  • Use hooks for state that affects rendering
  • Use getState() for mutations in callbacks
  • Don't subscribe when you don't need to

Pitfall 3.2: Not Using Selectors for Optimization

SUBOPTIMAL:

function UserAvatar() {
  // Re-renders on ANY auth state change! ❌
  const { user, accessToken, isLoading, isAuthenticated } = useAuthStore();

  return <Avatar src={user?.avatar} />;
}

OPTIMIZED:

function UserAvatar() {
  // Only re-renders when user changes ✅
  const user = useAuthStore((state) => state.user);

  return <Avatar src={user?.avatar} />;
}

Key Takeaway:

  • Use selectors for components that only need subset of state
  • Reduces unnecessary re-renders
  • Especially important in frequently updating stores

4. TypeScript Type Safety

Pitfall 4.1: Using any Type

WRONG:

function processUser(user: any) {
  // ❌ Loses all type safety
  return user.name.toUpperCase(); // No error if user.name is undefined
}

CORRECT:

function processUser(user: User | null) {
  if (!user?.name) {
    return '';
  }
  return user.name.toUpperCase();
}

Key Takeaway:

  • Never use any - use unknown if type is truly unknown
  • Define proper types for all function parameters
  • Use type guards for runtime checks

Pitfall 4.2: Implicit Types Leading to Errors

WRONG:

// No explicit return type - type inference can be wrong
export function useAuth() {
  const context = useContext(AuthContext);
  return context; // What type is this? ❌
}

CORRECT:

// Explicit return type with overloads
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return selector ? context(selector) : context();
}

Key Takeaway:

  • Always provide explicit return types for public APIs
  • Use function overloads for polymorphic functions
  • Document types in JSDoc comments

Pitfall 4.3: Not Using import type for Type-Only Imports

SUBOPTIMAL:

import { ReactNode } from 'react'; // Might be bundled even if only used for types

CORRECT:

import type { ReactNode } from 'react'; // Guaranteed to be stripped from bundle

Key Takeaway:

  • Use import type for type-only imports
  • Smaller bundle size
  • Clearer intent

5. Component Patterns

Pitfall 5.1: Forgetting Optional Chaining for Nullable Values

WRONG:

function UserProfile() {
  const { user } = useAuth();
  return <div>{user.name}</div>;  // ❌ Crashes if user is null
}

CORRECT:

function UserProfile() {
  const { user } = useAuth();

  if (!user) {
    return <div>Not logged in</div>;
  }

  return <div>{user.name}</div>;  // ✅ Safe
}

// OR with optional chaining
function UserProfile() {
  const { user } = useAuth();
  return <div>{user?.name ?? 'Guest'}</div>;  // ✅ Safe
}

Key Takeaway:

  • Always handle null/undefined cases
  • Use optional chaining (?.) and nullish coalescing (??)
  • Provide fallback UI for missing data

Pitfall 5.2: Mixing Concerns in Components

WRONG:

function UserDashboard() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);

  // Data fetching mixed with component logic ❌
  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data))
      .finally(() => setLoading(false));
  }, []);

  // Business logic mixed with rendering ❌
  const activeUsers = users.filter(u => u.isActive);
  const sortedUsers = activeUsers.sort((a, b) => a.name.localeCompare(b.name));

  return <div>{/* Render sortedUsers */}</div>;
}

CORRECT:

// Custom hook for data fetching
function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => UserService.getUsers(),
  });
}

// Custom hook for business logic
function useActiveUsersSorted(users: User[] | undefined) {
  return useMemo(() => {
    if (!users) return [];
    return users
      .filter(u => u.isActive)
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [users]);
}

// Component only handles rendering
function UserDashboard() {
  const { data: users, isLoading } = useUsers();
  const sortedUsers = useActiveUsersSorted(users);

  if (isLoading) return <LoadingSpinner />;

  return <div>{/* Render sortedUsers */}</div>;
}

Key Takeaway:

  • Separate concerns: data fetching, business logic, rendering
  • Extract logic to custom hooks
  • Keep components focused on UI

6. Provider Architecture

Pitfall 6.1: Wrong Provider Order

WRONG:

// AuthInitializer outside AuthProvider ❌
function RootLayout({ children }) {
  return (
    <Providers>
      <AuthInitializer />  {/* Can't access auth context! */}
      <AuthProvider>
        {children}
      </AuthProvider>
    </Providers>
  );
}

CORRECT:

function RootLayout({ children }) {
  return (
    <AuthProvider>           {/* Provider first */}
      <AuthInitializer />    {/* Can access auth context */}
      <Providers>
        {children}
      </Providers>
    </AuthProvider>
  );
}

Key Takeaway:

  • Providers must wrap components that use them
  • Order matters when there are dependencies
  • Keep provider tree shallow (performance)

Pitfall 6.2: Creating Too Many Providers

WRONG:

// Separate provider for every piece of state ❌
<UserProvider>
  <ThemeProvider>
    <LanguageProvider>
      <NotificationProvider>
        <SettingsProvider>
          <App />
        </SettingsProvider>
      </NotificationProvider>
    </LanguageProvider>
  </ThemeProvider>
</UserProvider>

BETTER:

// Combine related state, use Zustand for most things
<AuthProvider>         {/* Only for auth DI */}
  <ThemeProvider>      {/* Built-in from lib */}
    <QueryClientProvider>  {/* React Query */}
      <App />
    </QueryClientProvider>
  </ThemeProvider>
</AuthProvider>

// Most other state in Zustand stores (no providers needed)
const useUIStore = create(...);  // Theme, sidebar, modals
const useUserPreferences = create(...);  // User settings

Key Takeaway:

  • Use Context only when necessary (DI, third-party integrations)
  • Use Zustand for most global state (no provider needed)
  • Avoid provider hell

7. Event Handlers & Callbacks

Pitfall 7.1: Using Hooks in Event Handlers

WRONG:

function MyComponent() {
  const handleClick = () => {
    const { user } = useAuth();  // ❌ Hook called in callback!
    console.log(user);
  };

  return <button onClick={handleClick}>Click</button>;
}

CORRECT:

function MyComponent() {
  const { user } = useAuth();  // ✅ Hook at component top level

  const handleClick = () => {
    console.log(user);  // Access from closure
  };

  return <button onClick={handleClick}>Click</button>;
}

// OR for mutations, use getState()
function MyComponent() {
  const handleLogout = async () => {
    const clearAuth = useAuthStore.getState().clearAuth;  // ✅ Not a hook call
    await clearAuth();
  };

  return <button onClick={handleLogout}>Logout</button>;
}

Key Takeaway:

  • Never call hooks inside event handlers
  • For render state: Call hook at top level, access in closure
  • For mutations: Use store.getState().method()

Pitfall 7.2: Not Handling Async Errors in Event Handlers

WRONG:

const handleSubmit = async (data: FormData) => {
  await apiCall(data); // ❌ No error handling!
};

CORRECT:

const handleSubmit = async (data: FormData) => {
  try {
    await apiCall(data);
    toast.success('Success!');
  } catch (error) {
    console.error('Failed to submit:', error);
    toast.error('Failed to submit form');
  }
};

Key Takeaway:

  • Always wrap async calls in try/catch
  • Provide user feedback for both success and errors
  • Log errors for debugging

8. Testing Pitfalls

Pitfall 8.1: Not Mocking Context Providers in Tests

WRONG:

// Test without provider ❌
test('renders user name', () => {
  render(<UserProfile />);  // Will crash - no AuthProvider!
  expect(screen.getByText('John')).toBeInTheDocument();
});

CORRECT:

// Mock the hook
jest.mock('@/lib/stores', () => ({
  useAuth: jest.fn(),
}));

test('renders user name', () => {
  (useAuth as jest.Mock).mockReturnValue({
    user: { id: '1', name: 'John' },
    isAuthenticated: true,
  });

  render(<UserProfile />);
  expect(screen.getByText('John')).toBeInTheDocument();
});

Key Takeaway:

  • Mock hooks at module level in tests
  • Provide necessary return values for each test case
  • Test both success and error states

Pitfall 8.2: Testing Implementation Details

WRONG:

test('calls useAuthStore hook', () => {
  const spy = jest.spyOn(require('@/lib/stores'), 'useAuthStore');
  render(<MyComponent />);
  expect(spy).toHaveBeenCalled();  // ❌ Testing implementation!
});

CORRECT:

test('displays user name when authenticated', () => {
  (useAuth as jest.Mock).mockReturnValue({
    user: { name: 'John' },
    isAuthenticated: true,
  });

  render(<MyComponent />);
  expect(screen.getByText('John')).toBeInTheDocument();  // ✅ Testing behavior!
});

Key Takeaway:

  • Test behavior, not implementation
  • Focus on what the user sees/does
  • Don't test internal API calls unless critical

9. Performance

Pitfall 9.1: Not Using React.memo for Expensive Components

SUBOPTIMAL:

// Re-renders every time parent re-renders ❌
function ExpensiveChart({ data }) {
  // Heavy computation/rendering
  return <ComplexVisualization data={data} />;
}

OPTIMIZED:

// Only re-renders when data changes ✅
export const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
  return <ComplexVisualization data={data} />;
});

Key Takeaway:

  • Use React.memo for expensive components
  • Especially useful for list items, charts, heavy UI
  • Profile with React DevTools to identify candidates

Pitfall 9.2: Creating Functions Inside Render

SUBOPTIMAL:

function MyComponent() {
  return (
    <button onClick={() => console.log('clicked')}>  {/* New function every render */}
      Click
    </button>
  );
}

OPTIMIZED:

function MyComponent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return <button onClick={handleClick}>Click</button>;
}

When to Optimize:

  • For memoized child components (memo, PureComponent)
  • For expensive event handlers
  • When profiling shows performance issues

When NOT to optimize:

  • Simple components with cheap operations (premature optimization)
  • One-off event handlers

Key Takeaway:

  • Use useCallback for functions passed to memoized children
  • But don't optimize everything - profile first

10. Import/Export Patterns

Pitfall 10.1: Not Using Barrel Exports

INCONSISTENT:

// Deep imports all over the codebase
import { useAuth } from '@/lib/auth/AuthContext';
import { useAuthStore } from '@/lib/stores/authStore';
import { User } from '@/lib/stores/authStore';

CONSISTENT:

// Barrel exports in stores/index.ts
export { useAuth, AuthProvider } from '../auth/AuthContext';
export { useAuthStore, type User } from './authStore';

// Clean imports everywhere
import { useAuth, useAuthStore, User } from '@/lib/stores';

Key Takeaway:

  • Create barrel exports (index.ts) for public APIs
  • Easier to refactor internal structure
  • Consistent import paths across codebase

Pitfall 10.2: Circular Dependencies

WRONG:

// fileA.ts
import { functionB } from './fileB';
export function functionA() {
  return functionB();
}

// fileB.ts
import { functionA } from './fileA'; // ❌ Circular!
export function functionB() {
  return functionA();
}

CORRECT:

// utils.ts
export function sharedFunction() {
  /* shared logic */
}

// fileA.ts
import { sharedFunction } from './utils';
export function functionA() {
  return sharedFunction();
}

// fileB.ts
import { sharedFunction } from './utils';
export function functionB() {
  return sharedFunction();
}

Key Takeaway:

  • Avoid circular imports
  • Extract shared code to separate modules
  • Keep dependency graph acyclic

Verification Checklist

Before committing code, always run:

# Type checking
npm run type-check

# Linting
npm run lint

# Tests
npm test

# Build check
npm run build

In browser:

  • No console errors or warnings
  • Components render correctly
  • No infinite loops or excessive re-renders (React DevTools)
  • Proper error handling (test error states)

Additional Resources


Last Updated: 2025-11-03 Maintainer: Development Team Status: Living Document - Add new pitfalls as they're discovered