Next js

Module 5: Next Js State Management (Modern Patterns)

From React Hooks to Global State with Zustand and Redux Toolkit

Modern apps need to manage state โ€” local UI state, global UI state, and server-synced state. In this module, youโ€™ll learn when to use which solution and implement each one using TypeScript and App Router conventions.


๐Ÿง  5.1 Local State with useState and useEffect

For small, isolated state (like toggles, form inputs), use useState.

๐Ÿงช Example: Counter with useState

// src/components/Counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Count: {count}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="px-4 py-2 bg-blue-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

๐Ÿงช useEffect for Side Effects

"use client";
import { useEffect, useState } from "react";

export default function TimeNow() {
  const [time, setTime] = useState("");

  useEffect(() => {
    const now = new Date().toLocaleTimeString();
    setTime(now);
  }, []);

  return <p>Current Time: {time}</p>;
}

๐ŸŒ 5.2 Global State with useContext

For light shared state (e.g. theme, language), use Context.

๐Ÿงช

Project Directory Structure:

/src
  /components
    Counter.tsx
    ThemeButton.tsx
    TimeNow.tsx
  /context
    ThemeContext.tsx
  /app
    layout.tsx



Explanation of Each Folder/File:

  • /src/components/: Contains the React components for your app.

    • Counter.tsx: The component for the counter that uses useState to manage the count.

    • ThemeButton.tsx: The button to toggle between light and dark themes, using useContext to access the theme and toggle function from the context.

    • TimeNow.tsx: A component that displays the current time, using useEffect to fetch the time once on mount.

  • /src/context/: Contains files related to the appโ€™s global state management using useContext.

    • ThemeContext.tsx: Creates and provides the ThemeContext that stores the current theme (light or dark) and provides the toggle function to switch between them.

  • /src/app/: Contains the layout file that wraps the entire app.

    • layout.tsx: Wraps your app with ThemeProvider to provide the theme context to the entire app.


Final Directory Setup with Sample Files:

1. /src/components/Counter.tsx:

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Count: {count}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="px-4 py-2 bg-blue-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

 

2. /src/components/ThemeButton.tsx:

"use client";
import { useTheme } from "@/context/ThemeContext";

export default function ThemeButton() {
  const { theme, toggle } = useTheme();
  return (
    <button onClick={toggle} className="border p-2 rounded">
      Toggle Theme ({theme})
    </button>
  );
}

 

3. /src/components/TimeNow.tsx:

"use client";
import { useEffect, useState } from "react";

export default function TimeNow() {
  const [time, setTime] = useState("");

  useEffect(() => {
    const now = new Date().toLocaleTimeString();
    setTime(now);
  }, []);

  return <p>Current Time: {time}</p>;
}

4. /src/context/ThemeContext.tsx:

"use client";
import { createContext, useContext, useState } from "react";

type Theme = "light" | "dark";

const ThemeContext = createContext<{
  theme: Theme;
  toggle: () => void;
}>( {
  theme: "light",
  toggle: () => {},
});

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = useState<Theme>("light");

  const toggle = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      <div className={theme}>{children}</div>
    </ThemeContext.Provider>
  );
};

export const useTheme = () => useContext(ThemeContext);

5. /src/app/layout.tsx:

import { ThemeProvider } from "@/context/ThemeContext";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

 


โš–๏ธ 5.3 Zustand โ€“ Lightweight Global Store

Zustand is a minimalist state manager โ€” faster than Context, simpler than Redux.

๐Ÿงช Step 1: Create Store

// src/store/counter.ts
import { create } from "zustand";

type State = {
  count: number;
  increment: () => void;
};

export const useCounter = create<State>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

๐Ÿงช Step 2: Use Store

// src/components/ZustandCounter.tsx
"use client";
import { useCounter } from "@/store/counter";

export default function ZustandCounter() {
  const { count, increment } = useCounter();

  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Zustand Count: {count}</p>
      <button
        onClick={increment}
        className="px-4 py-2 bg-green-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

โœ… Ideal for simple, global, shared state like theme, auth, or filters.


๐Ÿข 5.4 Redux Toolkit โ€“ Enterprise Global State

Use Redux Toolkit when:

  • Your state is large
  • You need middleware, API caching, or debug tools
  • Multiple modules share deeply nested state

๐Ÿงช Step 1: Setup Redux

npm install @reduxjs/toolkit react-redux

๐Ÿงช Step 2: Create Store & Slice

// src/store/redux/store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// src/store/redux/counterSlice.ts
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value += 1;
    },
  },
});

export const { increment } = counterSlice.actions;
export default counterSlice.reducer;

๐Ÿงช Step 3: Use with Provider

// src/app/layout.tsx
import { Provider } from "react-redux";
import { store } from "@/store/redux/store";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Provider store={store}>{children}</Provider>
      </body>
    </html>
  );
}

๐Ÿงช Step 4: Redux Counter Component

// src/components/ReduxCounter.tsx
"use client";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store/redux/store";
import { increment } from "@/store/redux/counterSlice";

export default function ReduxCounter() {
  const count = useSelector((state: RootState) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Redux Count: {count}</p>
      <button
        onClick={() => dispatch(increment())}
        className="px-4 py-2 bg-purple-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

Full Code

/*
Project: Next.js 14 State Management Examples
Each subfolder demonstrates a specific pattern:
- Local State (useState, useEffect)
- Context API (useContext)
- Zustand
- Redux Toolkit
*/

// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// 1. Local State
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// src/components/state/Counter.tsx
"use client";
import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Count: {count}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="px-4 py-2 bg-blue-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

// src/components/state/TimeNow.tsx
"use client";
import { useEffect, useState } from "react";

export default function TimeNow() {
  const [time, setTime] = useState("");
  useEffect(() => {
    const now = new Date().toLocaleTimeString();
    setTime(now);
  }, []);
  return <p>Current Time: {time}</p>;
}

// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// 2. Context API
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// src/context/ThemeContext.tsx
"use client";
import { createContext, useContext, useState } from "react";

type Theme = "light" | "dark";

const ThemeContext = createContext({
  theme: "light" as Theme,
  toggle: () => {},
});

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = useState<Theme>("light");
  const toggle = () => setTheme((prev) => (prev === "light" ? "dark" : "light"));
  return <ThemeContext.Provider value={{ theme, toggle }}><div className={theme}>{children}</div></ThemeContext.Provider>;
};

export const useTheme = () => useContext(ThemeContext);

// src/components/context/ThemeButton.tsx
"use client";
import { useTheme } from "@/context/ThemeContext";

export default function ThemeButton() {
  const { theme, toggle } = useTheme();
  return (
    <button onClick={toggle} className="border p-2 rounded">
      Toggle Theme ({theme})
    </button>
  );
}

// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// 3. Zustand Store
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// src/store/zustand/counter.ts
import { create } from "zustand";

type State = {
  count: number;
  increment: () => void;
};

export const useCounter = create<State>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// src/components/zustand/ZustandCounter.tsx
"use client";
import { useCounter } from "@/store/zustand/counter";

export default function ZustandCounter() {
  const { count, increment } = useCounter();
  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Zustand Count: {count}</p>
      <button
        onClick={increment}
        className="px-4 py-2 bg-green-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// 4. Redux Toolkit
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// src/store/redux/counterSlice.ts
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value += 1;
    },
  },
});

export const { increment } = counterSlice.actions;
export default counterSlice.reducer;

// src/store/redux/store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// src/components/redux/ReduxCounter.tsx
"use client";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store/redux/store";
import { increment } from "@/store/redux/counterSlice";

export default function ReduxCounter() {
  const count = useSelector((state: RootState) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div className="p-4 border rounded w-fit">
      <p className="text-xl mb-2">Redux Count: {count}</p>
      <button
        onClick={() => dispatch(increment())}
        className="px-4 py-2 bg-purple-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

// src/app/layout.tsx (Redux + Context)
import { ThemeProvider } from "@/context/ThemeContext";
import { Provider } from "react-redux";
import { store } from "@/store/redux/store";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Provider store={store}>
          <ThemeProvider>{children}</ThemeProvider>
        </Provider>
      </body>
    </html>
  );
}

 


โœ… Summary

Youโ€™ve learned:

Tool Best For
useState Local, simple UI state
useContext Lightweight global settings (theme/lang)
Zustand Fast, simple, scalable state
Redux Toolkit Large-scale, shared app logic + middleware

๐Ÿ”œ Coming Up Next:

In Module 6, youโ€™ll learn how to fetch data in Server & Client Components, use caching, and compare SSR vs SSG vs ISR in practice.

Would you like this converted to blog-ready Markdown as well?

Leave a Reply

Your email address will not be published. Required fields are marked *