DEV Community

Cover image for Redux Counter App Step by Step
Alifa Ara Heya
Alifa Ara Heya

Posted on

Redux Counter App Step by Step

Redux Counter App (with Vite + React + TypeScript)

A simple counter application built using React, Redux Toolkit, TypeScript, and Vite.


πŸ› οΈ Project Setup

1. Create a Vite + React App

npm create vite@latest redux-counter-app -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

Replace redux-counter-app with your desired project name. When prompted which language to use, you can select typescript.

2. Navigate to the Project Directory

cd redux-counter-app
Enter fullscreen mode Exit fullscreen mode

3. Install Dependencies

npm install
Enter fullscreen mode Exit fullscreen mode

4. Start the Development Server

npm run dev
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:5173 in your browser to view the app.


🧠 Setting Up Redux

Install Redux Toolkit and React-Redux

Add the Redux Toolkit and React-Redux packages to your project:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

5. Create the Redux Store

Create a file named src/redux/store.ts. Import the configureStore API from Redux Toolkit. We'll start by creating an empty Redux store, and exporting it:

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

export const store = configureStore({
  reducer: {}, // We'll add reducers here later
});
Enter fullscreen mode Exit fullscreen mode

This creates a Redux store, and also automatically configure the Redux DevTools extension so that you can inspect the store while developing.

6. Connect Redux Store to React

Once the store is created, we can make it available to our React components by putting a React-Redux <Provider> around our application.
Wrap your application with the Redux <Provider> in main.tsx and pass the store as a prop:

// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { Provider } from "react-redux";
import { store } from "./redux/store.ts";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

βœ… You can confirm Redux is connected by checking for @@INIT in the Redux DevTools.


🧩 Create a Redux State Slice

7. Setup counterSlice.ts

Organize your folder as follows:
src/redux/features/counter/counterSlice.ts. In that file, import the createSlice API from Redux Toolkit.
Creating a slice requires:

  • a string name to identify the slice,
  • an initial state value,
  • and one or more reducer functions to define how the state can be updated.
// src/redux/features/counter/counterSlice.ts
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  count: 0,
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {},
});

export default counterSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

πŸ”—8. Add the Reducer to the Store

Update store.ts to include the counterSlice.ts:

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

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
Enter fullscreen mode Exit fullscreen mode

βœ…In Redux DevTools, your state should now look like:

{
  "counter": {
    "count": 0
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Adding our business logic in reducers. This is our action.

update counterSlice.ts

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  count: 0,
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.count = state.count + 1;
    },
    decrement: (state) => {
      state.count = state.count - 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;

export default counterSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Why counterSlice.reducer and not counterSlice.reducers?

Because:

reducers (plural) is just your input β€” an object containing individual reducer functions.

reducer (singular) is the output β€” a single function Redux uses to manage that slice of state.

10. Add types in store.ts

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode

11. Create Typed Redux Hooks

Create src/redux/hook.ts:

import { useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "./store";

export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
Enter fullscreen mode Exit fullscreen mode

These typed hooks will provide full TypeScript support when using Redux in components.

12. Use Redux State and Actions in React Components

// src/App.tsx
// import { useDispatch } from "react-redux";
import { decrement, increment } from "./redux/features/counter/counterSlice";
// import type { RootState } from "./redux/store";
import { useAppDispatch, useAppSelector } from "./redux/hook";

function App() {
  // const counter = useSelector((state) => state.counter);
  // console.log(counter);

  // const { count } = useSelector((state: RootState) => state.counter);
  const { count } = useAppSelector((state) => state.counter);
  // console.log(count);
  // const dispatch = useDispatch();
  const dispatch = useAppDispatch();

  const handleIncrement = () => {
    dispatch(increment()); //make sure to call the increment function
  };

  const handleDecrement = () => {
    dispatch(decrement());
  };

  return (
    <>
      <h1>Counter With Redux</h1>
      <button aria-label="Increment value" onClick={handleIncrement}>
        Increment
      </button>
      <div>{count}</div>
      <button aria-label="Decrement value" onClick={handleDecrement}>
        Decrement
      </button>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

13. Make Actions Dynamic with Payloads

Update counterSlice.ts to accept payloads for increment/decrement, so that we can increment/decrement not just 1, but any number we want:

let's update our counterSlice.ts

// src/redux/features/counter/counterSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

const initialState = {
  count: 0,
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state, action: PayloadAction<number>) => {
      state.count += action.payload;
    },
    decrement: (state, action: PayloadAction<number>) => {
      state.count -= action.payload;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Now your actions are flexible and can take dynamic values like 1, 5, or any number.

let's update our App.tsx again for final touch.

import { decrement, increment } from "./redux/features/counter/counterSlice";

import { useAppDispatch, useAppSelector } from "./redux/hook";

function App() {
  const { count } = useAppSelector((state) => state.counter);

  const dispatch = useAppDispatch();

  const handleIncrement = (amount: number) => {
    dispatch(increment(amount)); //make sure to call the increment function
  };

  const handleDecrement = (amount: number) => {
    dispatch(decrement(amount));
  };

  return (
    <>
      <h1>Counter With Redux</h1>
      <button aria-label="Increment value" onClick={() => handleIncrement(1)}>
        Increment
      </button>
      <button
        aria-label="Increment value by 5"
        onClick={() => handleIncrement(5)}
      >
        Increment value by 5
      </button>
      <div>{count}</div>
      <button aria-label="Decrement value" onClick={() => handleDecrement(1)}>
        Decrement
      </button>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

🧩 14. Redux Middleware – Logger

In this section, we're adding a custom middleware to our Redux store to log every action dispatched and view the state before and after the action is applied. This is helpful for debugging and understanding how state changes in your app.


πŸ“‚ Create redux/middlewares/logger.ts

// currying concept

const logger = (state) => (next) => (action) => {
    console.group(action.type); // Start a collapsible console group with action type
    console.info('previous state', state.getState()); // Log the previous state
    const result = next(action); // Call the next middleware or reducer
    console.info('next state', state.getState()); // Log the updated state
    console.groupEnd(); // End the console group
    return result; // Return the result of next(action)
}

export default logger;
Enter fullscreen mode Exit fullscreen mode

βœ… What This Middleware Does

This is a logger middleware created using function currying. Here's how it works:

  1. logger(state) β€” receives the store object as state.
  2. Returns a function that takes next, the next middleware in the chain (or the reducer).
  3. That returns another function which finally takes the dispatched action.

Every time you dispatch an action, it:

  • Logs the action type.
  • Shows the state before the action.
  • Passes the action to the next middleware/reducer using next(action).
  • Logs the state after the action has been processed.
  • Groups these logs under the action type for cleaner console output.

πŸ§ͺ Add it to store.ts

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from './features/counter/counterSlice';
import logger from "./middlewares/logger";

export const store = configureStore({
    reducer: {
        counter: counterReducer
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(logger) // Add logger after default middleware
});
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Why Use It?

This middleware helps you see what’s happening under the hood of your Redux store, making it easier to debug state changes during development.

Top comments (0)