Home / Articles / Context vs Redux Toolkit vs Zustand for React state

This article is published in English.

Context vs Redux Toolkit vs Zustand for React state

Compare Context, Redux Toolkit, and Zustand for a video portal, then combine client stores with TanStack Query for server state.

7272 words

1. Introduction

Choosing how React state is owned is one of the architectural decisions that ages poorly when it is wrong. Local useState is enough for isolated UI, yet apps quickly need shared auth, theme, player status, and server caches. The toolkit for that work has grown with React itself.

Early React relied on local state and prop drilling. Flux (2014) and then Redux (2015) made a single predictable store with reducers the default for large teams. MobX (2015) offered observables as a lighter alternative. React 16.3 (2018) shipped a modern Context API so global values could move without an external library. React 16.8 (2019) added useReducer for Redux-like transitions inside React. Zustand (2019) reduced boilerplate with a hooks-first store. Jotai (2020) and Recoil (2020) pushed atomic and derived-state models. TanStack Query (formerly React Query, 2020) separated server-state concerns—caching, pagination, mutations—from client UI state.

A working map looks like this:

  • Local useState — UI flags and values that never leave one component.
  • Context — Medium apps with low-frequency globals (theme, auth, locale).
  • useReducer + Context — Complex transitions that still should stay in React.
  • Redux Toolkit — Large teams, heavy middleware, DevTools, and enterprise workflows.
  • Zustand — Medium-to-large apps that want a small API and strong performance.
  • TanStack Query — Server state, regardless of which client store you pick.

A video-portal sketch applies the same domains—theme, auth, courses, player—under Context, Redux Toolkit, and Zustand, then combines them with TanStack Query.

2. Concepts with Detailed Explanation

Context API — Built-in Global State

Context passes values through the tree without threading props. It is built into React, needs no package, and fits medium-frequency updates such as theme, authentication, language, and feature flags.

Three pieces make up the pattern: createContext() builds the context (optional default), Context.Provider publishes a value, and useContext(Context) reads it in descendants. When the provider value changes, every consumer re-renders.

That broadcast cost matters. Consumers re-render even if they only need one field. Mitigations include splitting contexts by update rate, memoizing provider values with useMemo, or selective consumer helpers such as use-context-selector.

A video portal can keep theme, auth, and player state in separate contexts:

// contexts/ThemeContext.tsx
'use client';

import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';

type Theme = 'light' | 'dark' | 'system';
type ActualTheme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  actualTheme: ActualTheme;
  setTheme: (theme: Theme) => void;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>(() => {
    if (typeof window !== 'undefined') {
      const saved = localStorage.getItem('theme') as Theme;
      if (saved && ['light', 'dark', 'system'].includes(saved)) return saved;
    }
    return 'system';
  });

  const [actualTheme, setActualTheme] = useState<ActualTheme>('light');

  // Detect system preference
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

    const updateActualTheme = () => {
      if (theme === 'system') {
        setActualTheme(mediaQuery.matches ? 'dark' : 'light');
      } else {
        setActualTheme(theme as ActualTheme);
      }
    };

    updateActualTheme();
    mediaQuery.addEventListener('change', updateActualTheme);
    return () => mediaQuery.removeEventListener('change', updateActualTheme);
  }, [theme]);

  // Persist to localStorage and apply to document
  useEffect(() => {
    localStorage.setItem('theme', theme);

    if (actualTheme === 'dark') {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }, [theme, actualTheme]);

  const toggleTheme = useCallback(() => {
    setTheme(prev => {
      if (prev === 'light') return 'dark';
      if (prev === 'dark') return 'system';
      return 'light';
    });
  }, []);

  const value = useMemo(() => ({
    theme,
    actualTheme,
    setTheme,
    toggleTheme,
  }), [theme, actualTheme, toggleTheme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}
// contexts/AuthContext.tsx
'use client';

import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import { useRouter } from 'next/navigation';

interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  role: 'user' | 'instructor' | 'admin';
}

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  loginWithGoogle: () => void;
  logout: () => void;
  register: (name: string, email: string, password: string) => Promise<void>;
  hasPermission: (permission: string) => boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const router = useRouter();

  // Check for existing session on mount
  useEffect(() => {
    const checkSession = async () => {
      try {
        const response = await fetch('/api/auth/session');
        if (response.ok) {
          const userData = await response.json();
          setUser(userData);
        }
      } catch (error) {
        console.error('Session check failed:', error);
      } finally {
        setIsLoading(false);
      }
    };

    checkSession();
  }, []);

  const login = useCallback(async (email: string, password: string) => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) throw new Error('Login failed');

      const userData = await response.json();
      setUser(userData);
      router.push('/dashboard');
    } finally {
      setIsLoading(false);
    }
  }, [router]);

  const loginWithGoogle = useCallback(() => {
    window.location.href = '/api/auth/google';
  }, []);

  const logout = useCallback(async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    setUser(null);
    router.push('/');
  }, [router]);

  const register = useCallback(async (name: string, email: string, password: string) => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, email, password }),
      });

      if (!response.ok) throw new Error('Registration failed');

      const userData = await response.json();
      setUser(userData);
      router.push('/dashboard');
    } finally {
      setIsLoading(false);
    }
  }, [router]);

  const hasPermission = useCallback((permission: string): boolean => {
    if (!user) return false;
    if (user.role === 'admin') return true;
    // Check specific permissions based on role
    const permissions: Record<string, string[]> = {
      user: ['view_courses', 'enroll_courses', 'leave_reviews'],
      instructor: ['view_courses', 'create_courses', 'edit_courses', 'view_analytics'],
      admin: ['*'],
    };
    return permissions[user.role]?.includes(permission) || permissions[user.role]?.includes('*') || false;
  }, [user]);

  const value = useMemo(() => ({
    user,
    isLoading,
    isAuthenticated: !!user,
    login,
    loginWithGoogle,
    logout,
    register,
    hasPermission,
  }), [user, isLoading, login, loginWithGoogle, logout, register, hasPermission]);

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

export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}
// contexts/VideoPlayerContext.tsx - Complex state with Context + useReducer
'use client';

import { createContext, useContext, useReducer, useCallback, ReactNode } from 'react';

// State types
interface VideoState {
  isPlaying: boolean;
  currentTime: number;
  duration: number;
  volume: number;
  isMuted: boolean;
  playbackRate: number;
  quality: 'auto' | '1080p' | '720p' | '480p';
  subtitlesEnabled: boolean;
  subtitleTrack: string | null;
  bookmarks: Bookmark[];
  showNotes: boolean;
  notes: string;
}

interface Bookmark {
  id: string;
  timestamp: number;
  note: string;
}

// Action types
type VideoAction =
  | { type: 'PLAY' }
  | { type: 'PAUSE' }
  | { type: 'TOGGLE_PLAY' }
  | { type: 'SET_TIME'; payload: number }
  | { type: 'SET_DURATION'; payload: number }
  | { type: 'SET_VOLUME'; payload: number }
  | { type: 'TOGGLE_MUTE' }
  | { type: 'SET_PLAYBACK_RATE'; payload: number }
  | { type: 'SET_QUALITY'; payload: VideoState['quality'] }
  | { type: 'TOGGLE_SUBTITLES' }
  | { type: 'ADD_BOOKMARK'; payload: { timestamp: number; note: string } }
  | { type: 'REMOVE_BOOKMARK'; payload: string }
  | { type: 'TOGGLE_NOTES' }
  | { type: 'SET_NOTES'; payload: string }
  | { type: 'RESET' };

// Reducer
const initialState: VideoState = {
  isPlaying: false,
  currentTime: 0,
  duration: 0,
  volume: 80,
  isMuted: false,
  playbackRate: 1,
  quality: 'auto',
  subtitlesEnabled: false,
  subtitleTrack: null,
  bookmarks: [],
  showNotes: false,
  notes: '',
};

function videoReducer(state: VideoState, action: VideoAction): VideoState {
  switch (action.type) {
    case 'PLAY':
      return { ...state, isPlaying: true };
    case 'PAUSE':
      return { ...state, isPlaying: false };
    case 'TOGGLE_PLAY':
      return { ...state, isPlaying: !state.isPlaying };
    case 'SET_TIME':
      return { ...state, currentTime: Math.max(0, Math.min(action.payload, state.duration)) };
    case 'SET_DURATION':
      return { ...state, duration: action.payload };
    case 'SET_VOLUME': {
      const newVolume = Math.max(0, Math.min(100, action.payload));
      return { ...state, volume: newVolume, isMuted: newVolume === 0 };
    }
    case 'TOGGLE_MUTE':
      return { ...state, isMuted: !state.isMuted };
    case 'SET_PLAYBACK_RATE':
      return { ...state, playbackRate: action.payload };
    case 'SET_QUALITY':
      return { ...state, quality: action.payload };
    case 'TOGGLE_SUBTITLES':
      return { ...state, subtitlesEnabled: !state.subtitlesEnabled };
    case 'ADD_BOOKMARK': {
      const newBookmark: Bookmark = {
        id: Date.now().toString(),
        timestamp: action.payload.timestamp,
        note: action.payload.note,
      };
      return { ...state, bookmarks: [...state.bookmarks, newBookmark] };
    }
    case 'REMOVE_BOOKMARK':
      return { ...state, bookmarks: state.bookmarks.filter(b => b.id !== action.payload) };
    case 'TOGGLE_NOTES':
      return { ...state, showNotes: !state.showNotes };
    case 'SET_NOTES':
      return { ...state, notes: action.payload };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

// Context type
interface VideoPlayerContextType {
  state: VideoState;
  play: () => void;
  pause: () => void;
  togglePlay: () => void;
  setTime: (time: number) => void;
  setDuration: (duration: number) => void;
  setVolume: (volume: number) => void;
  toggleMute: () => void;
  setPlaybackRate: (rate: number) => void;
  setQuality: (quality: VideoState['quality']) => void;
  toggleSubtitles: () => void;
  addBookmark: (timestamp: number, note: string) => void;
  removeBookmark: (id: string) => void;
  toggleNotes: () => void;
  setNotes: (notes: string) => void;
  reset: () => void;
}

const VideoPlayerContext = createContext<VideoPlayerContextType | undefined>(undefined);

export function VideoPlayerProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(videoReducer, initialState);

  const play = useCallback(() => dispatch({ type: 'PLAY' }), []);
  const pause = useCallback(() => dispatch({ type: 'PAUSE' }), []);
  const togglePlay = useCallback(() => dispatch({ type: 'TOGGLE_PLAY' }), []);
  const setTime = useCallback((time: number) => dispatch({ type: 'SET_TIME', payload: time }), []);
  const setDuration = useCallback((duration: number) => dispatch({ type: 'SET_DURATION', payload: duration }), []);
  const setVolume = useCallback((volume: number) => dispatch({ type: 'SET_VOLUME', payload: volume }), []);
  const toggleMute = useCallback(() => dispatch({ type: 'TOGGLE_MUTE' }), []);
  const setPlaybackRate = useCallback((rate: number) => dispatch({ type: 'SET_PLAYBACK_RATE', payload: rate }), []);
  const setQuality = useCallback((quality: VideoState['quality']) => dispatch({ type: 'SET_QUALITY', payload: quality }), []);
  const toggleSubtitles = useCallback(() => dispatch({ type: 'TOGGLE_SUBTITLES' }), []);
  const addBookmark = useCallback((timestamp: number, note: string) =>
    dispatch({ type: 'ADD_BOOKMARK', payload: { timestamp, note } }), []);
  const removeBookmark = useCallback((id: string) => dispatch({ type: 'REMOVE_BOOKMARK', payload: id }), []);
  const toggleNotes = useCallback(() => dispatch({ type: 'TOGGLE_NOTES' }), []);
  const setNotes = useCallback((notes: string) => dispatch({ type: 'SET_NOTES', payload: notes }), []);
  const reset = useCallback(() => dispatch({ type: 'RESET' }), []);

  const value = useMemo(() => ({
    state,
    play,
    pause,
    togglePlay,
    setTime,
    setDuration,
    setVolume,
    toggleMute,
    setPlaybackRate,
    setQuality,
    toggleSubtitles,
    addBookmark,
    removeBookmark,
    toggleNotes,
    setNotes,
    reset,
  }), [state, play, pause, togglePlay, setTime, setDuration, setVolume, toggleMute,
      setPlaybackRate, setQuality, toggleSubtitles, addBookmark, removeBookmark,
      toggleNotes, setNotes, reset]);

  return (
    <VideoPlayerContext.Provider value={value}>
      {children}
    </VideoPlayerContext.Provider>
  );
}

export function useVideoPlayer() {
  const context = useContext(VideoPlayerContext);
  if (context === undefined) {
    throw new Error('useVideoPlayer must be used within a VideoPlayerProvider');
  }
  return context;
}

Providers compose at the layout root:

// app/layout.tsx - App with multiple Context providers
import { ThemeProvider } from '@/contexts/ThemeContext';
import { AuthProvider } from '@/contexts/AuthContext';
import { VideoPlayerProvider } from '@/contexts/VideoPlayerContext';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ThemeProvider>
          <AuthProvider>
            <VideoPlayerProvider>
              {children}
            </VideoPlayerProvider>
          </AuthProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}

Consumers stay thin:

// components/ThemeToggle.tsx - Using Context
'use client';

import { useTheme } from '@/contexts/ThemeContext';
import { Sun, Moon, Monitor } from 'lucide-react';

export function ThemeToggle() {
  const { theme, setTheme, actualTheme } = useTheme();

  return (
    <div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
      <button
        onClick={() => setTheme('light')}
        className={`p-2 rounded ${theme === 'light' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''}`}
        aria-label="Light mode"
      >
        <Sun className="w-4 h-4" />
      </button>
      <button
        onClick={() => setTheme('dark')}
        className={`p-2 rounded ${theme === 'dark' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''}`}
        aria-label="Dark mode"
      >
        <Moon className="w-4 h-4" />
      </button>
      <button
        onClick={() => setTheme('system')}
        className={`p-2 rounded ${theme === 'system' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''}`}
        aria-label="System preference"
      >
        <Monitor className="w-4 h-4" />
      </button>
    </div>
  );
}

Context is simple to adopt and sufficient when updates are infrequent. High-frequency player ticks or large fan-out trees usually want a store with narrow selectors instead.

Redux Toolkit for large coordinated stores

RTK is the supported toolkit for current Redux apps. It removes classic ceremony and includes helpers for configuring the store, defining slices, writing async thunks, and fetching with RTK Query.

Core building blocks:

  • Slices — Colocate reducers and actions in one file via createSlice.
  • createAsyncThunk — Standard pending/fulfilled/rejected flows for async work.
  • RTK Query — Data fetching and cache integrated with the store.
  • Selectors — Derived, memoized reads from state.
  • Middleware — Thunk (included) and logging or other enhancers as needed.

RTK fits large codebases with tangled state interactions, many contributors, time-travel debugging, deep middleware needs, and a preference for strictly predictable updates.

A video portal store might wire courses, player, preferences, and an API slice:

// store/index.ts - Redux store configuration
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import coursesReducer from './slices/coursesSlice';
import videoPlayerReducer from './slices/videoPlayerSlice';
import userPreferencesReducer from './slices/userPreferencesSlice';
import { apiSlice } from './services/apiSlice';

export const store = configureStore({
  reducer: {
    courses: coursesReducer,
    videoPlayer: videoPlayerReducer,
    userPreferences: userPreferencesReducer,
    [apiSlice.reducerPath]: apiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
  devTools: process.env.NODE_ENV !== 'production',
});

// Enable refetchOnFocus/refetchOnReconnect behaviors
setupListeners(store.dispatch);

// Infer types for TypeScript
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// store/hooks.ts - Typed hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// store/slices/coursesSlice.ts - Courses slice with createSlice and createAsyncThunk
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

interface Course {
  id: string;
  title: string;
  description: string;
  thumbnail: string;
  price: number;
  rating: number;
  enrolledCount: number;
  instructor: {
    id: string;
    name: string;
    avatar: string;
  };
  isEnrolled: boolean;
}

interface CoursesState {
  items: Course[];
  featured: Course[];
  recommended: Course[];
  searchResults: Course[];
  currentCourse: Course | null;
  isLoading: boolean;
  error: string | null;
  filters: {
    category: string | null;
    level: string | null;
    sortBy: 'newest' | 'popular' | 'rating' | 'price_low' | 'price_high';
    searchTerm: string;
  };
  pagination: {
    page: number;
    totalPages: number;
    totalItems: number;
  };
}

const initialState: CoursesState = {
  items: [],
  featured: [],
  recommended: [],
  searchResults: [],
  currentCourse: null,
  isLoading: false,
  error: null,
  filters: {
    category: null,
    level: null,
    sortBy: 'popular',
    searchTerm: '',
  },
  pagination: {
    page: 1,
    totalPages: 1,
    totalItems: 0,
  },
};

// Async thunks
export const fetchCourses = createAsyncThunk(
  'courses/fetchCourses',
  async (params: { page?: number; category?: string; sortBy?: string } = {}) => {
    const queryParams = new URLSearchParams();
    if (params.page) queryParams.append('page', params.page.toString());
    if (params.category) queryParams.append('category', params.category);
    if (params.sortBy) queryParams.append('sort', params.sortBy);

    const response = await fetch(`/api/courses?${queryParams}`);
    if (!response.ok) throw new Error('Failed to fetch courses');
    return response.json();
  }
);

export const fetchCourseById = createAsyncThunk(
  'courses/fetchCourseById',
  async (courseId: string) => {
    const response = await fetch(`/api/courses/${courseId}`);
    if (!response.ok) throw new Error('Failed to fetch course');
    return response.json();
  }
);

export const fetchFeaturedCourses = createAsyncThunk(
  'courses/fetchFeaturedCourses',
  async () => {
    const response = await fetch('/api/courses/featured');
    return response.json();
  }
);

export const fetchRecommendedCourses = createAsyncThunk(
  'courses/fetchRecommendedCourses',
  async () => {
    const response = await fetch('/api/courses/recommended');
    return response.json();
  }
);

export const searchCourses = createAsyncThunk(
  'courses/searchCourses',
  async (searchTerm: string) => {
    const response = await fetch(`/api/courses/search?q=${encodeURIComponent(searchTerm)}`);
    return response.json();
  }
);

export const enrollInCourse = createAsyncThunk(
  'courses/enrollInCourse',
  async (courseId: string, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/courses/${courseId}/enroll`, {
        method: 'POST',
      });
      if (!response.ok) {
        const error = await response.json();
        return rejectWithValue(error.message);
      }
      return { courseId, enrolled: true };
    } catch (error) {
      return rejectWithValue('Enrollment failed');
    }
  }
);

const coursesSlice = createSlice({
  name: 'courses',
  initialState,
  reducers: {
    setFilters: (state, action: PayloadAction<Partial<CoursesState['filters']>>) => {
      state.filters = { ...state.filters, ...action.payload };
      state.pagination.page = 1; // Reset pagination when filters change
    },
    resetFilters: (state) => {
      state.filters = initialState.filters;
      state.pagination.page = 1;
    },
    setPage: (state, action: PayloadAction<number>) => {
      state.pagination.page = action.payload;
    },
    clearSearchResults: (state) => {
      state.searchResults = [];
      state.filters.searchTerm = '';
    },
    updateCourseEnrollment: (state, action: PayloadAction<{ courseId: string; isEnrolled: boolean }>) => {
      const { courseId, isEnrolled } = action.payload;

      // Update in items list
      const itemIndex = state.items.findIndex(c => c.id === courseId);
      if (itemIndex !== -1) {
        state.items[itemIndex].isEnrolled = isEnrolled;
        if (isEnrolled) {
          state.items[itemIndex].enrolledCount += 1;
        } else {
          state.items[itemIndex].enrolledCount -= 1;
        }
      }

      // Update in featured list
      const featuredIndex = state.featured.findIndex(c => c.id === courseId);
      if (featuredIndex !== -1) {
        state.featured[featuredIndex].isEnrolled = isEnrolled;
      }

      // Update current course
      if (state.currentCourse?.id === courseId) {
        state.currentCourse.isEnrolled = isEnrolled;
      }
    },
  },
  extraReducers: (builder) => {
    builder
      // fetchCourses
      .addCase(fetchCourses.pending, (state) => {
        state.isLoading = true;
        state.error = null;
      })
      .addCase(fetchCourses.fulfilled, (state, action) => {
        state.isLoading = false;
        state.items = action.payload.courses;
        state.pagination = {
          page: action.payload.page,
          totalPages: action.payload.totalPages,
          totalItems: action.payload.total,
        };
      })
      .addCase(fetchCourses.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.error.message || 'Failed to fetch courses';
      })
      // fetchCourseById
      .addCase(fetchCourseById.pending, (state) => {
        state.isLoading = true;
        state.currentCourse = null;
      })
      .addCase(fetchCourseById.fulfilled, (state, action) => {
        state.isLoading = false;
        state.currentCourse = action.payload;
      })
      .addCase(fetchCourseById.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.error.message || 'Failed to fetch course';
      })
      // fetchFeaturedCourses
      .addCase(fetchFeaturedCourses.fulfilled, (state, action) => {
        state.featured = action.payload;
      })
      // fetchRecommendedCourses
      .addCase(fetchRecommendedCourses.fulfilled, (state, action) => {
        state.recommended = action.payload;
      })
      // searchCourses
      .addCase(searchCourses.fulfilled, (state, action) => {
        state.searchResults = action.payload;
      })
      // enrollInCourse
      .addCase(enrollInCourse.fulfilled, (state, action) => {
        const { courseId, enrolled } = action.payload;
        coursesSlice.caseReducers.updateCourseEnrollment(state, {
          type: 'courses/updateCourseEnrollment',
          payload: { courseId, isEnrolled: enrolled },
        });
      });
  },
});

export const { setFilters, resetFilters, setPage, clearSearchResults, updateCourseEnrollment } = coursesSlice.actions;
export default coursesSlice.reducer;
// store/slices/videoPlayerSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface Bookmark {
  id: string;
  timestamp: number;
  note: string;
}

interface VideoPlayerState {
  isPlaying: boolean;
  currentTime: number;
  duration: number;
  volume: number;
  isMuted: boolean;
  playbackRate: number;
  quality: 'auto' | '1080p' | '720p' | '480p';
  subtitlesEnabled: boolean;
  subtitlesTrack: string | null;
  bookmarks: Bookmark[];
  showNotes: boolean;
  notes: string;
  currentVideoId: string | null;
  currentCourseId: string | null;
}

const initialState: VideoPlayerState = {
  isPlaying: false,
  currentTime: 0,
  duration: 0,
  volume: 80,
  isMuted: false,
  playbackRate: 1,
  quality: 'auto',
  subtitlesEnabled: false,
  subtitlesTrack: null,
  bookmarks: [],
  showNotes: false,
  notes: '',
  currentVideoId: null,
  currentCourseId: null,
};

const videoPlayerSlice = createSlice({
  name: 'videoPlayer',
  initialState,
  reducers: {
    setCurrentVideo: (state, action: PayloadAction<{ videoId: string; courseId: string }>) => {
      state.currentVideoId = action.payload.videoId;
      state.currentCourseId = action.payload.courseId;
      state.currentTime = 0;
      state.isPlaying = false;
      state.bookmarks = [];
    },
    play: (state) => {
      state.isPlaying = true;
    },
    pause: (state) => {
      state.isPlaying = false;
    },
    togglePlay: (state) => {
      state.isPlaying = !state.isPlaying;
    },
    setTime: (state, action: PayloadAction<number>) => {
      state.currentTime = Math.max(0, Math.min(action.payload, state.duration));
    },
    setDuration: (state, action: PayloadAction<number>) => {
      state.duration = action.payload;
    },
    setVolume: (state, action: PayloadAction<number>) => {
      const volume = Math.max(0, Math.min(100, action.payload));
      state.volume = volume;
      if (volume === 0) state.isMuted = true;
      else if (state.isMuted) state.isMuted = false;
    },
    toggleMute: (state) => {
      state.isMuted = !state.isMuted;
    },
    setPlaybackRate: (state, action: PayloadAction<number>) => {
      state.playbackRate = action.payload;
    },
    setQuality: (state, action: PayloadAction<VideoPlayerState['quality']>) => {
      state.quality = action.payload;
    },
    toggleSubtitles: (state) => {
      state.subtitlesEnabled = !state.subtitlesEnabled;
    },
    addBookmark: (state, action: PayloadAction<{ timestamp: number; note: string }>) => {
      const newBookmark: Bookmark = {
        id: Date.now().toString(),
        timestamp: action.payload.timestamp,
        note: action.payload.note,
      };
      state.bookmarks.push(newBookmark);
    },
    removeBookmark: (state, action: PayloadAction<string>) => {
      state.bookmarks = state.bookmarks.filter(b => b.id !== action.payload);
    },
    toggleNotes: (state) => {
      state.showNotes = !state.showNotes;
    },
    setNotes: (state, action: PayloadAction<string>) => {
      state.notes = action.payload;
    },
    seekForward: (state) => {
      state.currentTime = Math.min(state.duration, state.currentTime + 10);
    },
    seekBackward: (state) => {
      state.currentTime = Math.max(0, state.currentTime - 10);
    },
    resetPlayer: (state) => {
      return { ...initialState, currentCourseId: state.currentCourseId };
    },
  },
});

export const {
  setCurrentVideo,
  play,
  pause,
  togglePlay,
  setTime,
  setDuration,
  setVolume,
  toggleMute,
  setPlaybackRate,
  setQuality,
  toggleSubtitles,
  addBookmark,
  removeBookmark,
  toggleNotes,
  setNotes,
  seekForward,
  seekBackward,
  resetPlayer,
} = videoPlayerSlice.actions;

export default videoPlayerSlice.reducer;
// store/slices/userPreferencesSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface UserPreferencesState {
  theme: 'light' | 'dark' | 'system';
  language: string;
  autoplay: boolean;
  quality: 'auto' | '1080p' | '720p' | '480p';
  subtitlesEnabled: boolean;
  subtitlesLanguage: string;
  volume: number;
  playbackRate: number;
  notifications: {
    email: boolean;
    push: boolean;
    courseUpdates: boolean;
    newContent: boolean;
  };
  recentSearches: string[];
  bookmarksView: 'grid' | 'list';
  sortPreferences: {
    courses: 'newest' | 'popular' | 'rating';
    videos: 'recent' | 'popular';
  };
}

const loadFromLocalStorage = (): Partial<UserPreferencesState> => {
  if (typeof window === 'undefined') return {};
  try {
    const saved = localStorage.getItem('userPreferences');
    if (saved) return JSON.parse(saved);
  } catch (error) {
    console.error('Failed to load preferences:', error);
  }
  return {};
};

const initialState: UserPreferencesState = {
  theme: 'system',
  language: 'en',
  autoplay: true,
  quality: 'auto',
  subtitlesEnabled: false,
  subtitlesLanguage: 'en',
  volume: 80,
  playbackRate: 1,
  notifications: {
    email: true,
    push: false,
    courseUpdates: true,
    newContent: true,
  },
  recentSearches: [],
  bookmarksView: 'grid',
  sortPreferences: {
    courses: 'popular',
    videos: 'recent',
  },
  ...loadFromLocalStorage(),
};

const userPreferencesSlice = createSlice({
  name: 'userPreferences',
  initialState,
  reducers: {
    setTheme: (state, action: PayloadAction<UserPreferencesState['theme']>) => {
      state.theme = action.payload;
      this.persist(state);
    },
    setLanguage: (state, action: PayloadAction<string>) => {
      state.language = action.payload;
      this.persist(state);
    },
    setAutoplay: (state, action: PayloadAction<boolean>) => {
      state.autoplay = action.payload;
      this.persist(state);
    },
    setQuality: (state, action: PayloadAction<UserPreferencesState['quality']>) => {
      state.quality = action.payload;
      this.persist(state);
    },
    setSubtitlesEnabled: (state, action: PayloadAction<boolean>) => {
      state.subtitlesEnabled = action.payload;
      this.persist(state);
    },
    setSubtitlesLanguage: (state, action: PayloadAction<string>) => {
      state.subtitlesLanguage = action.payload;
      this.persist(state);
    },
    setVolume: (state, action: PayloadAction<number>) => {
      state.volume = action.payload;
      this.persist(state);
    },
    setPlaybackRate: (state, action: PayloadAction<number>) => {
      state.playbackRate = action.payload;
      this.persist(state);
    },
    updateNotifications: (state, action: PayloadAction<Partial<UserPreferencesState['notifications']>>) => {
      state.notifications = { ...state.notifications, ...action.payload };
      this.persist(state);
    },
    addRecentSearch: (state, action: PayloadAction<string>) => {
      const term = action.payload.trim();
      if (!term) return;
      state.recentSearches = [
        term,
        ...state.recentSearches.filter(s => s !== term),
      ].slice(0, 10);
      this.persist(state);
    },
    clearRecentSearches: (state) => {
      state.recentSearches = [];
      this.persist(state);
    },
    setBookmarksView: (state, action: PayloadAction<'grid' | 'list'>) => {
      state.bookmarksView = action.payload;
      this.persist(state);
    },
    setSortPreference: (state, action: PayloadAction<{ type: 'courses' | 'videos'; value: string }>) => {
      state.sortPreferences[action.payload.type] = action.payload.value as any;
      this.persist(state);
    },
    resetPreferences: () => initialState,
  },
});

// Helper to persist to localStorage
function persist(state: UserPreferencesState) {
  if (typeof window === 'undefined') return;
  const { ...persistable } = state;
  localStorage.setItem('userPreferences', JSON.stringify(persistable));
}

export const {
  setTheme,
  setLanguage,
  setAutoplay,
  setQuality,
  setSubtitlesEnabled,
  setSubtitlesLanguage,
  setVolume,
  setPlaybackRate,
  updateNotifications,
  addRecentSearch,
  clearRecentSearches,
  setBookmarksView,
  setSortPreference,
  resetPreferences,
} = userPreferencesSlice.actions;

export default userPreferencesSlice.reducer;
// store/services/apiSlice.ts - RTK Query for data fetching
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export interface Course {
  id: string;
  title: string;
  description: string;
  thumbnail: string;
  price: number;
  rating: number;
  enrolledCount: number;
  instructor: {
    id: string;
    name: string;
    avatar: string;
  };
  isEnrolled?: boolean;
}

export interface Video {
  id: string;
  title: string;
  duration: number;
  url: string;
  thumbnail: string;
  order: number;
  isFree: boolean;
}

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers) => {
      const token = localStorage.getItem('access_token');
      if (token) {
        headers.set('authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ['Course', 'Courses', 'Video', 'Progress', 'Bookmark'],
  endpoints: (builder) => ({
    // Query endpoints
    getCourses: builder.query<{ courses: Course[]; total: number; page: number }, { page?: number; category?: string; sort?: string }>({
      query: (params) => ({
        url: '/courses',
        params,
      }),
      providesTags: (result) =>
        result
          ? [
              ...result.courses.map(({ id }) => ({ type: 'Course' as const, id })),
              { type: 'Courses', id: 'LIST' },
            ]
          : [{ type: 'Courses', id: 'LIST' }],
    }),

    getCourse: builder.query<Course, string>({
      query: (id) => `/courses/${id}`,
      providesTags: (result, error, id) => [{ type: 'Course', id }],
    }),

    getCourseVideos: builder.query<Video[], string>({
      query: (courseId) => `/courses/${courseId}/videos`,
      providesTags: (result, error, courseId) => [
        { type: 'Course', id: courseId },
        { type: 'Video', id: 'LIST' },
      ],
    }),

    getVideoProgress: builder.query<{ position: number; percentage: number; completed: boolean }, string>({
      query: (videoId) => `/progress/${videoId}`,
      providesTags: (result, error, videoId) => [{ type: 'Progress', id: videoId }],
    }),

    getBookmarks: builder.query<Video[], void>({
      query: () => '/bookmarks',
      providesTags: ['Bookmark'],
    }),

    // Mutation endpoints
    enrollInCourse: builder.mutation<{ success: boolean }, string>({
      query: (courseId) => ({
        url: `/courses/${courseId}/enroll`,
        method: 'POST',
      }),
      invalidatesTags: (result, error, courseId) => [
        { type: 'Course', id: courseId },
        { type: 'Courses', id: 'LIST' },
      ],
    }),

    updateProgress: builder.mutation<void, { videoId: string; position: number; percentage: number }>({
      query: ({ videoId, position, percentage }) => ({
        url: `/progress/${videoId}`,
        method: 'POST',
        body: { position, percentage },
      }),
      invalidatesTags: (result, error, { videoId }) => [
        { type: 'Progress', id: videoId },
      ],
    }),

    toggleBookmark: builder.mutation<{ isBookmarked: boolean }, string>({
      query: (videoId) => ({
        url: `/bookmarks/${videoId}`,
        method: 'POST',
      }),
      invalidatesTags: ['Bookmark'],
      async onQueryStarted(videoId, { dispatch, queryFulfilled }) {
        // Optimistic update example
        const patchResult = dispatch(
          apiSlice.util.updateQueryData('getBookmarks', undefined, (draft) => {
            // Optimistically add/remove bookmark
          })
        );
        try {
          await queryFulfilled;
        } catch {
          patchResult.undo();
        }
      },
    }),

    submitReview: builder.mutation<void, { courseId: string; rating: number; comment: string }>({
      query: ({ courseId, rating, comment }) => ({
        url: `/courses/${courseId}/reviews`,
        method: 'POST',
        body: { rating, comment },
      }),
      invalidatesTags: (result, error, { courseId }) => [{ type: 'Course', id: courseId }],
    }),
  }),
});

// Auto-generated hooks
export const {
  useGetCoursesQuery,
  useGetCourseQuery,
  useGetCourseVideosQuery,
  useGetVideoProgressQuery,
  useGetBookmarksQuery,
  useEnrollInCourseMutation,
  useUpdateProgressMutation,
  useToggleBookmarkMutation,
  useSubmitReviewMutation,
} = apiSlice;

Components then dispatch and select through typed hooks:

// components/CourseListRedux.tsx - React component using Redux
'use client';

import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { fetchCourses, setFilters, setPage } from '@/store/slices/coursesSlice';
import { useGetCoursesQuery } from '@/store/services/apiSlice';
import { RootState } from '@/store';

export function CourseListRedux() {
  // Using traditional Redux slices
  const dispatch = useAppDispatch();
  const { items: courses, isLoading, filters, pagination } = useAppSelector(
    (state: RootState) => state.courses
  );

  // Using RTK Query for automatic caching
  const { data: apiCourses, isLoading: apiLoading } = useGetCoursesQuery({
    page: pagination.page,
    category: filters.category || undefined,
    sort: filters.sortBy,
  });

  useEffect(() => {
    dispatch(fetchCourses({
      page: pagination.page,
      category: filters.category || undefined,
      sortBy: filters.sortBy,
    }));
  }, [dispatch, pagination.page, filters.category, filters.sortBy]);

  const handleFilterChange = (category: string | null) => {
    dispatch(setFilters({ category }));
  };

  const handleSortChange = (sortBy: typeof filters.sortBy) => {
    dispatch(setFilters({ sortBy }));
  };

  if (isLoading) {
    return <div className="flex justify-center p-8"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div></div>;
  }

  return (
    <div className="container mx-auto px-4 py-8">
      {/* Filters */}
      <div className="mb-6 flex gap-4">
        <select
          value={filters.category || ''}
          onChange={(e) => handleFilterChange(e.target.value || null)}
          className="px-3 py-2 border rounded-lg"
        >
          <option value="">All Categories</option>
          <option value="react">React</option>
          <option value="typescript">TypeScript</option>
          <option value="nextjs">Next.js</option>
        </select>

        <select
          value={filters.sortBy}
          onChange={(e) => handleSortChange(e.target.value as any)}
          className="px-3 py-2 border rounded-lg"
        >
          <option value="newest">Newest</option>
          <option value="popular">Most Popular</option>
          <option value="rating">Highest Rated</option>
          <option value="price_low">Price: Low to High</option>
          <option value="price_high">Price: High to Low</option>
        </select>
      </div>

      {/* Course Grid */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {courses.map((course) => (
          <CourseCardRedux key={course.id} course={course} />
        ))}
      </div>

      {/* Pagination */}
      {pagination.totalPages > 1 && (
        <div className="mt-8 flex justify-center gap-2">
          {Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((p) => (
            <button
              key={p}
              onClick={() => dispatch(setPage(p))}
              className={`px-3 py-1 rounded ${
                p === pagination.page
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-200 hover:bg-gray-300'
              }`}
            >
              {p}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

function CourseCardRedux({ course }: { course: Course }) {
  const [enroll] = useEnrollInCourseMutation();

  return (
    <div className="bg-white rounded-lg shadow-sm overflow-hidden">
      <img src={course.thumbnail} alt={course.title} className="w-full h-48 object-cover" />
      <div className="p-4">
        <h3 className="font-semibold text-lg">{course.title}</h3>
        <p className="text-sm text-gray-600 mt-1">{course.instructor.name}</p>
        <div className="flex items-center gap-2 mt-2">
          <span className="text-yellow-500">★</span>
          <span>{course.rating}</span>
          <span className="text-gray-400">•</span>
          <span>{course.enrolledCount.toLocaleString()} students</span>
        </div>
        <div className="mt-3 flex justify-between items-center">
          <span className="text-xl font-bold">${course.price}</span>
          {!course.isEnrolled ? (
            <button
              onClick={() => enroll(course.id)}
              className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
            >
              Enroll
            </button>
          ) : (
            <span className="px-4 py-2 bg-green-100 text-green-700 rounded">Enrolled ✓</span>
          )}
        </div>
      </div>
    </div>
  );
}

RTK’s structure scales and documents well; the trade-off is more ceremony than Context or Zustand for small screens.

Zustand — Minimal Boilerplate State Management

Zustand targets teams that want store semantics without Redux’s ceremony—especially when performance and a short API matter more than a formal action vocabulary.

A portal can split course and player concerns across Zustand stores with persist, DevTools, and related middleware:

// store/zustand/courseStore.ts
import { create } from 'zustand';
import { persist, devtools, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface Course {
  id: string;
  title: string;
  description: string;
  thumbnail: string;
  price: number;
  rating: number;
  enrolledCount: number;
  instructor: {
    id: string;
    name: string;
    avatar: string;
  };
  isEnrolled: boolean;
}

interface FilterState {
  category: string | null;
  level: string | null;
  sortBy: 'newest' | 'popular' | 'rating' | 'price_low' | 'price_high';
  searchTerm: string;
}

interface CourseStore {
  // State
  courses: Course[];
  featured: Course[];
  recommended: Course[];
  currentCourse: Course | null;
  isLoading: boolean;
  error: string | null;
  filters: FilterState;
  page: number;
  totalPages: number;
  hasMore: boolean;

  // Actions
  fetchCourses: () => Promise<void>;
  fetchCourseById: (id: string) => Promise<void>;
  fetchFeatured: () => Promise<void>;
  fetchRecommended: () => Promise<void>;
  searchCourses: (term: string) => Promise<void>;
  enrollInCourse: (courseId: string) => Promise<boolean>;
  setFilter: <K extends keyof FilterState>(key: K, value: FilterState[K]) => void;
  resetFilters: () => void;
  setPage: (page: number) => void;
  clearError: () => void;

  // Selectors (computed)
  getFilteredAndSortedCourses: () => Course[];
}

const initialFilters: FilterState = {
  category: null,
  level: null,
  sortBy: 'popular',
  searchTerm: '',
};

// Create store with multiple middleware
export const useCourseStore = create<CourseStore>()(
  devtools(
    persist(
      subscribeWithSelector(
        immer((set, get) => ({
          // Initial state
          courses: [],
          featured: [],
          recommended: [],
          currentCourse: null,
          isLoading: false,
          error: null,
          filters: initialFilters,
          page: 1,
          totalPages: 1,
          hasMore: true,

          // Actions
          fetchCourses: async () => {
            set({ isLoading: true, error: null }, false, 'courses/fetch/start');

            try {
              const { page, filters } = get();
              const params = new URLSearchParams();
              params.append('page', page.toString());
              if (filters.category) params.append('category', filters.category);
              if (filters.sortBy) params.append('sort', filters.sortBy);
              if (filters.searchTerm) params.append('search', filters.searchTerm);

              const response = await fetch(`/api/courses?${params}`);
              if (!response.ok) throw new Error('Failed to fetch courses');

              const data = await response.json();

              set((state) => {
                state.courses = page === 1 ? data.courses : [...state.courses, ...data.courses];
                state.totalPages = data.totalPages;
                state.hasMore = data.hasMore;
                state.isLoading = false;
              }, false, 'courses/fetch/success');
            } catch (error) {
              set({ error: (error as Error).message, isLoading: false }, false, 'courses/fetch/error');
            }
          },

          fetchCourseById: async (id: string) => {
            set({ isLoading: true, error: null }, false, 'course/fetch/start');

            try {
              const response = await fetch(`/api/courses/${id}`);
              if (!response.ok) throw new Error('Course not found');

              const course = await response.json();
              set({ currentCourse: course, isLoading: false }, false, 'course/fetch/success');
            } catch (error) {
              set({ error: (error as Error).message, isLoading: false }, false, 'course/fetch/error');
            }
          },

          fetchFeatured: async () => {
            try {
              const response = await fetch('/api/courses/featured');
              const featured = await response.json();
              set({ featured }, false, 'courses/featured/success');
            } catch (error) {
              console.error('Failed to fetch featured:', error);
            }
          },

          fetchRecommended: async () => {
            try {
              const response = await fetch('/api/courses/recommended');
              const recommended = await response.json();
              set({ recommended }, false, 'courses/recommended/success');
            } catch (error) {
              console.error('Failed to fetch recommended:', error);
            }
          },

          searchCourses: async (term: string) => {
            if (!term.trim()) {
              set(
                (state) => {
                  state.filters.searchTerm = '';
                  state.page = 1;
                },
                false,
                'courses/search/clear'
              );
              await get().fetchCourses();
              return;
            }

            set(
              (state) => {
                state.filters.searchTerm = term;
                state.page = 1;
                state.isLoading = true;
              },
              false,
              'courses/search/start'
            );

            try {
              const response = await fetch(`/api/courses/search?q=${encodeURIComponent(term)}`);
              const data = await response.json();

              set(
                (state) => {
                  state.courses = data.courses;
                  state.totalPages = data.totalPages;
                  state.hasMore = data.hasMore;
                  state.isLoading = false;
                },
                false,
                'courses/search/success'
              );
            } catch (error) {
              set({ error: (error as Error).message, isLoading: false }, false, 'courses/search/error');
            }
          },

          enrollInCourse: async (courseId: string) => {
            try {
              const response = await fetch(`/api/courses/${courseId}/enroll`, {
                method: 'POST',
              });

              if (!response.ok) return false;

              // Update local state
              set((state) => {
                // Update in courses list
                const courseIndex = state.courses.findIndex(c => c.id === courseId);
                if (courseIndex !== -1) {
                  state.courses[courseIndex].isEnrolled = true;
                  state.courses[courseIndex].enrolledCount += 1;
                }

                // Update current course
                if (state.currentCourse?.id === courseId) {
                  state.currentCourse.isEnrolled = true;
                }
              }, false, 'courses/enroll/success');

              return true;
            } catch (error) {
              console.error('Enrollment failed:', error);
              return false;
            }
          },

          setFilter: <K extends keyof FilterState>(key: K, value: FilterState[K]) => {
            set(
              (state) => {
                state.filters[key] = value;
                state.page = 1; // Reset pagination on filter change
              },
              false,
              `courses/filter/${String(key)}`
            );
            get().fetchCourses();
          },

          resetFilters: () => {
            set(
              (state) => {
                state.filters = initialFilters;
                state.page = 1;
              },
              false,
              'courses/filters/reset'
            );
            get().fetchCourses();
          },

          setPage: (page: number) => {
            set({ page }, false, 'courses/page/set');
            get().fetchCourses();
          },

          clearError: () => set({ error: null }, false, 'courses/error/clear'),

          // Selector: get filtered and sorted courses (computed)
          getFilteredAndSortedCourses: () => {
            const { courses, filters } = get();
            let result = [...courses];

            // Apply filters
            if (filters.category) {
              // In real app, filter by category
            }

            if (filters.level) {
              // Filter by level
            }

            // Apply sorting
            switch (filters.sortBy) {
              case 'newest':
                result.sort((a, b) => 0); // Add sorting logic
                break;
              case 'popular':
                result.sort((a, b) => b.enrolledCount - a.enrolledCount);
                break;
              case 'rating':
                result.sort((a, b) => b.rating - a.rating);
                break;
              case 'price_low':
                result.sort((a, b) => a.price - b.price);
                break;
              case 'price_high':
                result.sort((a, b) => b.price - a.price);
                break;
            }

            return result;
          },
        }))
      ),
      {
        name: 'course-store', // localStorage key
        partialize: (state) => ({
          // Only persist these fields
          filters: state.filters,
          page: state.page,
        }),
      }
    ),
    { name: 'CourseStore' } // Redux DevTools name
  )
);

// Selector hooks for performance
export const useCourses = () => useCourseStore((state) => state.courses);
export const useFeatured = () => useCourseStore((state) => state.featured);
export const useRecommended = () => useCourseStore((state) => state.recommended);
export const useCurrentCourse = () => useCourseStore((state) => state.currentCourse);
export const useCourseLoading = () => useCourseStore((state) => state.isLoading);
export const useCourseError = () => useCourseStore((state) => state.error);
export const useFilters = () => useCourseStore((state) => state.filters);
export const usePagination = () => useCourseStore((state) => ({ page: state.page, totalPages: state.totalPages }));
// store/zustand/videoPlayerStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface Bookmark {
  id: string;
  timestamp: number;
  note: string;
}

interface VideoPlayerState {
  // State
  isPlaying: boolean;
  currentTime: number;
  duration: number;
  volume: number;
  isMuted: boolean;
  playbackRate: number;
  quality: 'auto' | '1080p' | '720p' | '480p';
  subtitlesEnabled: boolean;
  subtitlesTrack: string | null;
  bookmarks: Bookmark[];
  showNotes: boolean;
  notes: string;
  currentVideoId: string | null;
  currentCourseId: string | null;

  // Actions
  play: () => void;
  pause: () => void;
  togglePlay: () => void;
  setTime: (time: number) => void;
  setDuration: (duration: number) => void;
  setVolume: (volume: number) => void;
  toggleMute: () => void;
  setPlaybackRate: (rate: number) => void;
  setQuality: (quality: VideoPlayerState['quality']) => void;
  toggleSubtitles: () => void;
  addBookmark: (timestamp: number, note?: string) => void;
  removeBookmark: (id: string) => void;
  toggleNotes: () => void;
  setNotes: (notes: string) => void;
  seekForward: (seconds?: number) => void;
  seekBackward: (seconds?: number) => void;
  reset: () => void;
  loadVideo: (videoId: string, courseId: string) => void;

  // Computed
  getProgressPercentage: () => number;
  getFormattedTime: () => string;
}

export const useVideoPlayerStore = create<VideoPlayerState>()(
  devtools(
    persist(
      (set, get) => ({
        // Initial state
        isPlaying: false,
        currentTime: 0,
        duration: 0,
        volume: 80,
        isMuted: false,
        playbackRate: 1,
        quality: 'auto',
        subtitlesEnabled: false,
        subtitlesTrack: null,
        bookmarks: [],
        showNotes: false,
        notes: '',
        currentVideoId: null,
        currentCourseId: null,

        // Actions
        play: () => set({ isPlaying: true }, false, 'video/play'),
        pause: () => set({ isPlaying: false }, false, 'video/pause'),
        togglePlay: () => set((state) => ({ isPlaying: !state.isPlaying }), false, 'video/togglePlay'),

        setTime: (time: number) => set((state) => ({
          currentTime: Math.max(0, Math.min(time, state.duration))
        }), false, 'video/setTime'),

        setDuration: (duration: number) => set({ duration }, false, 'video/setDuration'),

        setVolume: (volume: number) => {
          const clampedVolume = Math.max(0, Math.min(100, volume));
          set((state) => ({
            volume: clampedVolume,
            isMuted: clampedVolume === 0 ? true : state.isMuted,
          }), false, 'video/setVolume');
        },

        toggleMute: () => set((state) => ({ isMuted: !state.isMuted }), false, 'video/toggleMute'),

        setPlaybackRate: (rate: number) => set({ playbackRate: rate }, false, 'video/setPlaybackRate'),

        setQuality: (quality: VideoPlayerState['quality']) => set({ quality }, false, 'video/setQuality'),

        toggleSubtitles: () => set((state) => ({
          subtitlesEnabled: !state.subtitlesEnabled
        }), false, 'video/toggleSubtitles'),

        addBookmark: (timestamp: number, note: string = '') => {
          const newBookmark: Bookmark = {
            id: Date.now().toString(),
            timestamp,
            note,
          };
          set((state) => ({
            bookmarks: [...state.bookmarks, newBookmark].sort((a, b) => a.timestamp - b.timestamp)
          }), false, 'video/addBookmark');
        },

        removeBookmark: (id: string) => set((state) => ({
          bookmarks: state.bookmarks.filter(b => b.id !== id)
        }), false, 'video/removeBookmark'),

        toggleNotes: () => set((state) => ({ showNotes: !state.showNotes }), false, 'video/toggleNotes'),

        setNotes: (notes: string) => set({ notes }, false, 'video/setNotes'),

        seekForward: (seconds: number = 10) => set((state) => ({
          currentTime: Math.min(state.duration, state.currentTime + seconds)
        }), false, 'video/seekForward'),

        seekBackward: (seconds: number = 10) => set((state) => ({
          currentTime: Math.max(0, state.currentTime - seconds)
        }), false, 'video/seekBackward'),

        reset: () => set({
          isPlaying: false,
          currentTime: 0,
          duration: 0,
          bookmarks: [],
          showNotes: false,
          notes: '',
        }, false, 'video/reset'),

        loadVideo: (videoId: string, courseId: string) => {
          // Reset and load new video
          set({
            currentVideoId: videoId,
            currentCourseId: courseId,
            currentTime: 0,
            duration: 0,
            isPlaying: false,
            bookmarks: [],
          }, false, 'video/load');

          // Fetch saved progress from API (async)
          fetch(`/api/progress/${videoId}`)
            .then(res => res.json())
            .then(data => {
              if (data.position) {
                set({ currentTime: data.position }, false, 'video/restoreProgress');
              }
            })
            .catch(console.error);
        },

        // Computed values
        getProgressPercentage: () => {
          const { currentTime, duration } = get();
          return duration > 0 ? (currentTime / duration) * 100 : 0;
        },

        getFormattedTime: () => {
          const { currentTime, duration } = get();
          const format = (seconds: number) => {
            const hrs = Math.floor(seconds / 3600);
            const mins = Math.floor((seconds % 3600) / 60);
            const secs = Math.floor(seconds % 60);
            if (hrs > 0) return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
            return `${mins}:${secs.toString().padStart(2, '0')}`;
          };
          return `${format(currentTime)} / ${format(duration)}`;
        },
      }),
      {
        name: 'video-player-store',
        partialize: (state) => ({
          volume: state.volume,
          playbackRate: state.playbackRate,
          quality: state.quality,
          subtitlesEnabled: state.subtitlesEnabled,
        }),
      }
    ),
    { name: 'VideoPlayerStore' }
  )
);

// Individual selector hooks for better performance
export const useIsPlaying = () => useVideoPlayerStore((state) => state.isPlaying);
export const useCurrentTime = () => useVideoPlayerStore((state) => state.currentTime);
export const useDuration = () => useVideoPlayerStore((state) => state.duration);
export const useVolume = () => useVideoPlayerStore((state) => state.volume);
export const usePlaybackRate = () => useVideoPlayerStore((state) => state.playbackRate);
export const useBookmarks = () => useVideoPlayerStore((state) => state.bookmarks);
export const useVideoPlayerActions = () => useVideoPlayerStore((state) => ({
  play: state.play,
  pause: state.pause,
  togglePlay: state.togglePlay,
  setTime: state.setTime,
  setVolume: state.setVolume,
  toggleMute: state.toggleMute,
  setPlaybackRate: state.setPlaybackRate,
  addBookmark: state.addBookmark,
  removeBookmark: state.removeBookmark,
  seekForward: state.seekForward,
  seekBackward: state.seekBackward,
  loadVideo: state.loadVideo,
}));

Components read narrow slices of store state:

// components/ZustandVideoPlayer.tsx - Component using Zustand store
'use client';

import { useEffect, useRef } from 'react';
import {
  useVideoPlayerStore,
  useIsPlaying,
  useCurrentTime,
  useDuration,
  useVolume,
  usePlaybackRate,
  useBookmarks,
  useVideoPlayerActions
} from '@/store/zustand/videoPlayerStore';
import { Play, Pause, Volume2, VolumeX, SkipBack, SkipForward, Bookmark } from 'lucide-react';

export function ZustandVideoPlayer({ videoUrl, videoId, courseId }: {
  videoUrl: string;
  videoId: string;
  courseId: string;
}) {
  const videoRef = useRef<HTMLVideoElement>(null);

  // Individual selectors for precise re-rendering
  const isPlaying = useIsPlaying();
  const currentTime = useCurrentTime();
  const duration = useDuration();
  const volume = useVolume();
  const playbackRate = usePlaybackRate();
  const bookmarks = useBookmarks();
  const progressPercentage = useVideoPlayerStore((state) => state.getProgressPercentage());

  // Actions
  const {
    play,
    pause,
    togglePlay,
    setTime,
    setDuration,
    setVolume,
    toggleMute,
    setPlaybackRate,
    addBookmark,
    seekForward,
    seekBackward,
  } = useVideoPlayerActions();

  // Sync video element with store
  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    if (isPlaying) {
      video.play();
    } else {
      video.pause();
    }
  }, [isPlaying]);

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    video.volume = volume / 100;
  }, [volume]);

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    video.playbackRate = playbackRate;
  }, [playbackRate]);

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    if (Math.abs(video.currentTime - currentTime) > 0.5) {
      video.currentTime = currentTime;
    }
  }, [currentTime]);

  const handleTimeUpdate = () => {
    if (videoRef.current) {
      const time = videoRef.current.currentTime;
      if (Math.abs(time - currentTime) > 0.1) {
        setTime(time);
      }
    }
  };

  const handleLoadedMetadata = () => {
    if (videoRef.current) {
      setDuration(videoRef.current.duration);
    }
  };

  const handleAddBookmark = () => {
    const note = prompt('Add a note for this bookmark:');
    addBookmark(currentTime, note || '');
  };

  const jumpToBookmark = (timestamp: number) => {
    setTime(timestamp);
    if (videoRef.current) {
      videoRef.current.currentTime = timestamp;
    }
  };

  return (
    <div className="bg-black rounded-lg overflow-hidden">
      <video
        ref={videoRef}
        src={videoUrl}
        className="w-full aspect-video"
        onTimeUpdate={handleTimeUpdate}
        onLoadedMetadata={handleLoadedMetadata}
        onPlay={play}
        onPause={pause}
      />

      <div className="p-4 bg-gray-900 text-white">
        {/* Progress Bar */}
        <div className="mb-3">
          <div className="flex justify-between text-sm mb-1">
            <span>{formatTime(currentTime)}</span>
            <span>{formatTime(duration)}</span>
          </div>
          <div className="relative">
            <input
              type="range"
              min="0"
              max={duration}
              value={currentTime}
              onChange={(e) => setTime(parseFloat(e.target.value))}
              className="w-full"
            />
            {/* Bookmark markers on progress bar */}
            <div className="absolute top-0 left-0 right-0 pointer-events-none">
              {bookmarks.map((bookmark) => (
                <div
                  key={bookmark.id}
                  className="absolute w-0.5 h-3 bg-yellow-500 -top-1"
                  style={{ left: `${(bookmark.timestamp / duration) * 100}%` }}
                  title={bookmark.note}
                />
              ))}
            </div>
          </div>
        </div>

        {/* Controls */}
        <div className="flex items-center gap-3 flex-wrap">
          <button
            onClick={togglePlay}
            className="p-2 bg-blue-600 rounded-full hover:bg-blue-700"
          >
            {isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
          </button>

          <button
            onClick={() => seekBackward(10)}
            className="p-2 bg-gray-700 rounded hover:bg-gray-600"
          >
            <SkipBack className="w-5 h-5" />
          </button>

          <button
            onClick={() => seekForward(10)}
            className="p-2 bg-gray-700 rounded hover:bg-gray-600"
          >
            <SkipForward className="w-5 h-5" />
          </button>

          <button
            onClick={handleAddBookmark}
            className="p-2 bg-gray-700 rounded hover:bg-gray-600"
          >
            <Bookmark className="w-5 h-5" />
          </button>

          <div className="flex items-center gap-2 ml-4">
            <button onClick={toggleMute} className="p-1">
              {volume === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
            </button>
            <input
              type="range"
              min="0"
              max="100"
              value={volume}
              onChange={(e) => setVolume(parseInt(e.target.value))}
              className="w-24"
            />
          </div>

          <select
            value={playbackRate}
            onChange={(e) => setPlaybackRate(parseFloat(e.target.value))}
            className="px-2 py-1 bg-gray-800 rounded text-sm"
          >
            <option value="0.5">0.5x</option>
            <option value="0.75">0.75x</option>
            <option value="1">1x</option>
            <option value="1.25">1.25x</option>
            <option value="1.5">1.5x</option>
            <option value="2">2x</option>
          </select>
        </div>

        {/* Bookmarks List */}
        {bookmarks.length > 0 && (
          <div className="mt-4 pt-3 border-t border-gray-700">
            <h4 className="text-sm font-medium mb-2">Bookmarks</h4>
            <div className="flex flex-wrap gap-2">
              {bookmarks.map((bookmark) => (
                <button
                  key={bookmark.id}
                  onClick={() => jumpToBookmark(bookmark.timestamp)}
                  className="px-2 py-1 bg-gray-800 rounded text-xs hover:bg-gray-700"
                >
                  {formatTime(bookmark.timestamp)} - {bookmark.note || 'Untitled'}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function formatTime(seconds: number): string {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${secs.toString().padStart(2, '0')}`;
}

Zustand stays small while still supporting persistence and selective renders. It is a strong default when Redux’s ceremony outweighs its benefits.

Comparison and Decision Framework

Pick by update frequency, team size, and whether the data is server-owned or client-owned. Context for sparse globals; RTK when enterprise tooling and middleware dominate; Zustand when a tiny store API is enough; TanStack Query whenever the source of truth is the network.

Decision Matrix

Match the tool to constraints: learning curve, boilerplate, DevTools, middleware depth, and how many writers touch the same state. Prefer the smallest option that keeps updates predictable under your team’s size.

Performance Comparison

Context re-renders broadly on value changes. Redux and Zustand can bind more narrowly. Measure player scrubbing and list filters under production-like fan-out before locking a choice.

Integration with TanStack Query

A durable split:

  • TanStack Query — Server state (cache, background refresh, mutations).
  • Zustand or Redux — Client UI state (preferences, ephemeral player UI).
  • Context — Rarely changing globals.
// store/combined.ts - Integration example
import { create } from 'zustand';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Zustand for client state
interface UIStore {
  sidebarOpen: boolean;
  theme: 'light' | 'dark';
  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
}

export const useUIStore = create<UIStore>((set) => ({
  sidebarOpen: true,
  theme: 'light',
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
  setTheme: (theme) => set({ theme }),
}));

// TanStack Query for server state
export function useCourses() {
  return useQuery({
    queryKey: ['courses'],
    queryFn: () => fetch('/api/courses').then(res => res.json()),
  });
}

export function useEnrollCourse() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (courseId: string) =>
      fetch(`/api/courses/${courseId}/enroll`, { method: 'POST' }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['courses'] });
      queryClient.invalidateQueries({ queryKey: ['enrollments'] });
    },
  });
}

// Component using both
function CoursePage() {
  const { theme } = useUIStore();
  const { data: courses, isLoading } = useCourses();
  const enrollMutation = useEnrollCourse();

  return (
    <div data-theme={theme}>
      {courses?.map(course => (
        <CourseCard
          key={course.id}
          course={course}
          onEnroll={() => enrollMutation.mutate(course.id)}
        />
      ))}
    </div>
  );
}

3. Closing

Context covers built-in globals such as theme, auth, and player. RTK covers slice-based enterprise state with thunks and RTK Query. Zustand covers low-boilerplate stores with persistence. The decision frame above chooses among them, and the combined pattern keeps TanStack Query on server state while Zustand or Redux own client state.

Start sparse globals in Context, move to Zustand or RTK when narrow selectors matter, and keep network caches in TanStack Query instead of those client stores.