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:
Explanation of Each Folder/File:
-
/src/components/
: Contains the React components for your app.-
Counter.tsx
: The component for the counter that usesuseState
to manage the count. -
ThemeButton.tsx
: The button to toggle between light and dark themes, usinguseContext
to access the theme andtoggle
function from the context. -
TimeNow.tsx
: A component that displays the current time, usinguseEffect
to fetch the time once on mount.
-
-
/src/context/
: Contains files related to the appโs global state management usinguseContext
.-
ThemeContext.tsx
: Creates and provides theThemeContext
that stores the current theme (light
ordark
) and provides thetoggle
function to switch between them.
-
-
/src/app/
: Contains the layout file that wraps the entire app.-
layout.tsx
: Wraps your app withThemeProvider
to provide the theme context to the entire app.
-
Final Directory Setup with Sample Files:
1. /src/components/Counter.tsx
:
2. /src/components/ThemeButton.tsx
:
3. /src/components/TimeNow.tsx
:
4. /src/context/ThemeContext.tsx
:
5. /src/app/layout.tsx
:
โ๏ธ 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?