Building a Scalable React App with Combined Reducers using useReducer Hook
🚀 Building a Scalable React App with Combined Reducers
Learn how to structure your React application using the reducer pattern for better state management
📁 Project Folder Structure
Before diving into the code, let's understand how to organize your project files. This structure keeps your reducers modular and maintainable:
├── 📁 library/
│ └── 📁 reducers/
│ ├── 📄 index.js (Root Reducer)
│ ├── 📄 counterReducer.js
│ └── 📄 userReducer.js
└── 📁 components/
└── 📄 Counter.jsx
✨ Why This Structure?
- Separation of Concerns: Each reducer handles a specific slice of state
- Scalability: Easy to add new reducers as your app grows
- Maintainability: Related logic is grouped together
- Reusability: Reducers can be imported and used in different components
🔧 Step 1: Counter Reducer
Create library/reducers/counterReducer.js to manage counter state:
export const counterInitialState = {
count: 0,
};
export function counterReducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "RESET":
return { ...state, count: 0 };
default:
return state;
}
}
👤 Step 2: User Reducer
Create library/reducers/userReducer.js to manage user form state:
export const initialUserState = {
name: "",
email: "",
phone: "",
message: "",
};
export function userReducer(state, action) {
switch (action.type) {
case "SET_USER":
return { ...state, ...action.payload };
case "CLEAR_USER":
return initialUserState;
default:
return state;
}
}
🎯 Step 3: Root Reducer (Combining Reducers)
Create library/reducers/index.js to combine all reducers:
import { counterInitialState, counterReducer } from "./counterReducer";
import { initialUserState, userReducer } from "./userReducer";
export const initialRootState = {
user: initialUserState,
counter: counterInitialState,
};
export function rootReducer(state, action) {
return {
user: userReducer(state.user, action),
counter: counterReducer(state.counter, action),
};
}
💡 How Root Reducer Works
The root reducer combines multiple reducers into a single state tree. When an action is dispatched, it passes through all reducers, but only the relevant reducer will handle it based on the action type.
⚛️ Step 4: React Component Implementation
Create components/Counter.jsx - the main component using the combined reducers:
"use client";
import { initialRootState, rootReducer } from "@/library/reducers";
import { useReducer, useState } from "react";
const Counter = () => {
const [submitted, setSubmitted] = useState(false);
const [state, dispatch] = useReducer(rootReducer, initialRootState);
const handleInputChange = (e) => {
const { name, value } = e.target;
dispatch({ type: "SET_USER", payload: { [name]: value } });
};
const handleSubmit = () => {
if (state.user.name && state.user.email && state.user.message) {
setSubmitted(true);
setTimeout(() => setSubmitted(false), 3000);
dispatch({ type: "CLEAR_USER" });
}
};
return (
<div className="min-h-screen w-full bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex">
{/* Left Side - Counter */}
<div className="w-1/2 flex items-center justify-center p-8 border-r border-purple-500/30">
<div className="text-center space-y-8">
<div className="space-y-2">
<h1 className="text-6xl font-bold text-white">Counter</h1>
<div className="h-1 w-32 bg-gradient-to-r from-purple-500 to-pink-500 mx-auto rounded-full"></div>
</div>
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-purple-500 to-pink-500 rounded-3xl blur-xl opacity-50"></div>
<div className="relative bg-slate-800/80 backdrop-blur-sm rounded-3xl p-12 border border-purple-500/30">
<div className="text-8xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
{state.counter.count}
</div>
</div>
</div>
<div className="flex gap-4 justify-center">
<button
onClick={() => dispatch({ type: "DECREMENT" })}
className="group relative px-8 py-4 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-semibold text-white text-lg shadow-lg hover:shadow-red-500/50 transition-all duration-300 hover:scale-105"
>
<span className="relative z-10">Decrement</span>
</button>
<button
onClick={() => dispatch({ type: "RESET" })}
className="px-8 py-4 bg-slate-700 hover:bg-slate-600 rounded-xl font-semibold text-white text-lg transition-all duration-300 hover:scale-105"
>
Reset
</button>
<button
onClick={() => dispatch({ type: "INCREMENT" })}
className="group relative px-8 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-xl font-semibold text-white text-lg shadow-lg hover:shadow-purple-500/50 transition-all duration-300 hover:scale-105"
>
<span className="relative z-10">Increment</span>
</button>
</div>
</div>
</div>
{/* Right Side - User Form */}
<div className="w-1/2 flex items-center justify-center p-8">
<div className="w-full max-w-md">
<div className="space-y-2 mb-8">
<h2 className="text-5xl font-bold text-white">Get in Touch</h2>
<div className="h-1 w-24 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
</div>
<div className="space-y-6">
<div>
<label className="block text-purple-300 text-sm font-medium mb-2">
Full Name
</label>
<input
type="text"
name="name"
value={state.user.name}
onChange={handleInputChange}
className="w-full px-4 py-3 bg-slate-800/50 border border-purple-500/30 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="John Doe"
/>
</div>
<div>
<label className="block text-purple-300 text-sm font-medium mb-2">
Email Address
</label>
<input
type="email"
name="email"
value={state.user.email}
onChange={handleInputChange}
className="w-full px-4 py-3 bg-slate-800/50 border border-purple-500/30 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="john@example.com"
/>
</div>
<div>
<label className="block text-purple-300 text-sm font-medium mb-2">
Phone Number
</label>
<input
type="tel"
name="phone"
value={state.user.phone}
onChange={handleInputChange}
className="w-full px-4 py-3 bg-slate-800/50 border border-purple-500/30 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="+1 (555) 000-0000"
/>
</div>
<div>
<label className="block text-purple-300 text-sm font-medium mb-2">
Message
</label>
<textarea
name="message"
value={state.user.message}
onChange={handleInputChange}
rows="4"
className="w-full px-4 py-3 bg-slate-800/50 border border-purple-500/30 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all resize-none"
placeholder="Your message here..."
></textarea>
</div>
<button
onClick={handleSubmit}
className="w-full py-4 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 rounded-lg font-semibold text-white text-lg shadow-lg hover:shadow-purple-500/50 transition-all duration-300 hover:scale-105"
>
Send Message
</button>
{submitted && (
<div className="p-4 bg-green-500/20 border border-green-500/50 rounded-lg text-green-300 text-center animate-pulse">
Message sent successfully! ✓
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default Counter;
🎓 Key Concepts Explained
1. useReducer Hook
The useReducer hook is an alternative to useState for managing complex state logic. It follows the Redux pattern:
const [state, dispatch] = useReducer(reducer, initialState);
2. Dispatch Actions
Actions are plain objects that describe what happened:
dispatch({ type: "INCREMENT" });
dispatch({ type: "SET_USER", payload: { name: "John" } });
3. Benefits of This Pattern
- Predictable State Updates: All state changes go through reducers
- Easier Testing: Reducers are pure functions
- Better Organization: State logic is separated from UI
- Scalability: Easy to add more reducers as needed
📝 How to Use This Code
- Create the folder structure as shown above
- Copy each code block into its respective file
- Make sure you have Tailwind CSS configured in your project
- Import and use the Counter component in your app
🚀 Next Steps
Try adding your own reducer! For example, create a themeReducer.js to manage dark/light mode, or a todoReducer.js to manage a todo list. Just follow the same pattern and add it to the root reducer.

Comments
Post a Comment