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:

📦 src/
├── 📁 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:

TypeScript counterReducer.js
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:

TypeScript userReducer.js
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:

TypeScript index.js
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:

React JSX Counter.jsx
"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

  1. Create the folder structure as shown above
  2. Copy each code block into its respective file
  3. Make sure you have Tailwind CSS configured in your project
  4. 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

Popular Posts