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
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
3. Install Dependencies
npm install
4. Start the Development Server
npm run dev
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
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
});
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>
);
β
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;
π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,
},
});
β
In Redux DevTools, your state should now look like:
{
"counter": {
"count": 0
}
}
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;
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;
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>();
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;
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;
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;
π§© 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;
β What This Middleware Does
This is a logger middleware created using function currying. Here's how it works:
-
logger(state)
β receives thestore
object asstate
. - Returns a function that takes
next
, the next middleware in the chain (or the reducer). - 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
});
π‘ 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)