Lorenzo GM

Insights on web development, software engineering, and modern tech practices

Lorenzo GM

Frontend State Management Best Practices

Personal thoughts on effective frontend state management strategies - prioritizing URL state, cookies, and minimizing React state

Front-EndState ManagementBest Practices
Frontend State Management Best Practices

Frontend State Management Best Practices

State management is one of the most critical aspects of frontend development, yet it's often overcomplicated. After years of experience building complex web applications, I've developed some opinionated thoughts on how to approach state management effectively.

The State Management Hierarchy

When deciding where to store state in your frontend application, I recommend following this priority order:

1. Always Try to Use the URL First

The URL should be your primary source of truth for application state whenever possible. This approach offers several key benefits:

  • Shareability: Users can bookmark and share specific application states
  • Browser navigation: Back/forward buttons work intuitively
  • SEO benefits: Search engines can index different states of your application
  • Debugging: The current state is always visible in the address bar
  • Persistence: State survives page refreshes without additional complexity
// Good: Using URL params for filters
const searchParams = useSearchParams();
const category = searchParams.get('category') || 'all';
const sortBy = searchParams.get('sort') || 'name';

// Bad: Storing the same data in React state
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');

2. Single Session Cookie as Fallback

When URL state isn't practical (sensitive data, too much data, or UX considerations), use a single session cookie properly handled in the frontend:

  • One session cookie: Avoid multiple cookies that need synchronization
  • Proper handling: Ensure the frontend can read and update the session appropriately
  • Clear expiration: Set reasonable expiration times
  • Security: Use HttpOnly and Secure flags when appropriate
// Good: Single session with structured data
const session = {
  userId: '123',
  preferences: { theme: 'dark', language: 'en' },
  cart: { items: [], total: 0 }
};

// Bad: Multiple separate cookies
// userIdCookie, themePreferenceCookie, cartCookie, etc.

3. Avoid Multiple Cookies

Multiple cookies create synchronization nightmares:

  • Sync complexity: Keeping multiple cookies in sync is error-prone
  • Performance: Multiple HTTP headers on every request
  • Debugging difficulty: State scattered across multiple storage locations
  • Race conditions: Updates to different cookies can create inconsistent states

4. React State Should Be the Exception

Context API, useState, and other React state management should be used sparingly:

  • Local UI state only: Form inputs, modal visibility, loading states
  • Temporary interactions: Hover states, focus management, animations
  • Derived state: Computations based on props or other state
// Good: Local UI state
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);

// Bad: Application state that should be in URL or session
const [currentUser, setCurrentUser] = useState(null);
const [shoppingCart, setShoppingCart] = useState([]);

Why This Hierarchy Matters

Complexity Reduction

By following this hierarchy, you minimize the number of state sources in your application. Each additional state source exponentially increases complexity:

  • 1 source: Simple
  • 2 sources: Need synchronization
  • 3+ sources: Complex state management with potential conflicts

Debugging and Development

When state is primarily in the URL and a single session:

  • Easier debugging: State is visible and manipulable
  • Better testing: Reproducible states through URL manipulation
  • Simpler onboarding: New developers can understand state flow quickly

User Experience

  • Predictable behavior: Users understand how browsers work
  • Better performance: Less JavaScript state management overhead
  • Accessibility: Screen readers and other tools work better with URL-based navigation

Implementation Strategies

URL State Management

// Custom hook for URL state
function useURLState(key, defaultValue) {
  const searchParams = useSearchParams();
  const router = useRouter();
  
  const value = searchParams.get(key) || defaultValue;
  
  const setValue = useCallback((newValue) => {
    const params = new URLSearchParams(searchParams);
    if (newValue === defaultValue) {
      params.delete(key);
    } else {
      params.set(key, newValue);
    }
    router.push(`?${params.toString()}`);
  }, [key, defaultValue, searchParams, router]);
  
  return [value, setValue];
}

Session Management

// Centralized session management
class SessionManager {
  private static readonly SESSION_KEY = 'app_session';
  
  static getSession() {
    try {
      const session = document.cookie
        .split('; ')
        .find(row => row.startsWith(this.SESSION_KEY))
        ?.split('=')[1];
      return session ? JSON.parse(decodeURIComponent(session)) : null;
    } catch {
      return null;
    }
  }
  
  static updateSession(updates) {
    const current = this.getSession() || {};
    const updated = { ...current, ...updates };
    document.cookie = `${this.SESSION_KEY}=${encodeURIComponent(JSON.stringify(updated))}; path=/; max-age=86400`;
  }
}

Common Pitfalls to Avoid

Over-Engineering State Management

Don't reach for complex state management libraries (Redux, Zustand, etc.) unless you've exhausted simpler options:

// Often unnecessary
const useGlobalStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  // ... hundreds of lines of state logic
}));

// Usually sufficient
const user = SessionManager.getSession()?.user;

Premature Optimization

Don't optimize state management before you have a clear understanding of your actual state needs:

  • Start with URL and session
  • Add React state only when necessary
  • Measure performance before adding complexity

Conclusion

Effective frontend state management isn't about using the most sophisticated tools—it's about choosing the right tool for each piece of state. By prioritizing URL state, using a single well-managed session, and treating React state as the exception rather than the rule, you'll build applications that are easier to develop, debug, and maintain.

Remember: the best state management solution is the simplest one that meets your requirements. Start simple and add complexity only when absolutely necessary.


Image credits: OpenReplay Blog - React State Management with Easy Peasy