Now that you have a better idea of how Redux works, let's look at the topic we set out to better understand in the first place. Why do React and Redux get mentioned together so often? To help answer this, take a look at the following component hierarchy for some arbitrary app:

What this app does isn't very important. The only detail we'll throw in there is that some of these components are responsible for managing state and transferring some of that state around in the form of props:

In the ideal setup, this data each component needs will flow neatly down from parent to child:

Unfortunately, outside of simple scenarios, what we would like to do isn't very realistic. Your typical app does a lot of state generating, processing, and transferring! One component may initiate a state change. Another component somewhere else will want to react to it. The props related to this state change may travel both down the tree (yay!) as well as up the tree (nooo!) to reach whatever component is relying on the data being transferred:

We've been guilty of this quite a few times ourselves as part of communicating something (variable value, function/event handler reference, etc.) from child to parent and beyond.
Now, there are a few problems that we need to recognize with data traveling willy nilly through our components:
One solution to our problems is Redux. What Redux allows you to do is have the state your app shares live inside its data store instead of being distributed across a bunch of components:

This approach solves several of our problems. If you want to share data from one part of your app with another, you can do that without having to navigate up and down your component hierarchy:

You can initiate a state change and involve only the components that care about it. This directness reduces a lot of overhead you would otherwise have to maintain around ensuring your data (and any changes to it) gets to its intended destination. Pretty cool, right?
I should point out that Redux isn't the only way to avoid shuffling props around. Context and useReducer can handle a lot of what we looked at, and we'll talk about when each of them makes sense at the end of this tutorial. For now, let's see how Redux fits in with React.
From an architectural point of view, the overview of Redux we saw in the introduction still holds:

Besides the store, we still have to work with actions, reducers, and all the other related things that make up the Redux party. The only difference is that our app is built using React, and this difference (and how it plays with Redux) is where we will focus our attention.
Onwards!
Redux doesn't know anything about React, and React doesn't know anything about Redux. The thing that connects them is a separate package called react-redux. It gives us a small number of pieces to work with, and we only need to care about three of them for now:
Here is how they fit together:

The Provider sits at the top of our app and holds on to the store. Any component below it can use useSelector to read from the store. When the part of the state that component selected changes, the component re-renders with the new value. Components can also call useDispatch to get the dispatch function and fire off actions. This is also the part where we don't have to call subscribe ourselves anymore. The hooks take care of that for us.
If you've used React for a bit, this should remind you of how context works. That's not a coincidence. The Provider uses context behind the scenes to pass the store down. The difference is that useSelector is smart about what causes a re-render. Your component only re-renders if the specific value it selected has changed.
To see all of this in action, we are going to build a small counter app. It has a counter component with buttons for increasing and decreasing a number, and a second component that tells us whether that number is even or odd. The two components have nothing to do with each other as far as React is concerned. They only share data through the Redux store:

We'll use Vite to set up our project. If you need a refresher, the Setting Up Your React Dev Environment Easily tutorial walks through the details. From your terminal, run the following command:
npm create vite@latest reduxcounter -- --template react
Depending on your version of Vite, you may be asked a few follow-up questions, such as which linter to use or whether to install and start right away. The defaults are fine. When it's all done, go into the new folder and install the dependencies:
cd reduxcounter
npm install
Next, we need to install Redux Toolkit and react-redux. Run this in the same folder:
npm install @reduxjs/toolkit react-redux
Both of these packages are needed. @reduxjs/toolkit includes Redux itself plus all of the helpers we used in the last tutorial (createSlice and configureStore). The react-redux package is the bridge to React.
The Vite template comes with some files we won't need. Delete the src/App.css file and the src/assets folder. We'll replace everything else as we go.
The first thing we'll do is create the slice that describes our counter's state and the ways it can change. In the src folder, create a new file called counterSlice.js and add the following:
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0 },
reducers: {
increase: (state) => {
state.count += 1;
},
decrease: (state) => {
state.count -= 1;
},
},
});
export const { increase, decrease } = counterSlice.actions;
export default counterSlice.reducer;
This is the same createSlice pattern we used with our colors. We have a name, an initial state, and two reducers. Remember that Redux Toolkit uses a library called Immer, which lets us write state.count += 1 without worrying about modifying the original state. It creates a new state for us behind the scenes.
We export the two action creators (increase and decrease) so that our components can use them, and we export the reducer as the default export so that our store can use it.
Next, still in the src folder, create a file called store.js with the following contents:
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
The counter key is important. It determines where our slice's state lives in the store. Our state will look like { counter: { count: 0 } }, which is why we'll read the number later with state.counter.count.
Now, we need to let React know about our store. Open src/main.jsx and replace what is there with the following:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";
import "./index.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);
The highlighted lines are where the Redux-specific work happens. We import Provider and our store, and then wrap our App component with the Provider and hand it the store through the store prop. Everything inside App, no matter how deep, now has access to the store.
It's time for our first component that uses the store. In the src folder, create a file called Counter.jsx and add the following:
import { useSelector, useDispatch } from "react-redux";
import { increase, decrease } from "./counterSlice";
function Counter() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div className="container">
<button
className="buttons"
aria-label="Decrease"
onClick={() => dispatch(decrease())}
>
-
</button>
<span>{count}</span>
<button
className="buttons"
aria-label="Increase"
onClick={() => dispatch(increase())}
>
+
</button>
</div>
);
}
export default Counter;
Let's look at the highlighted bits. The useSelector hook takes a function (called a selector) that receives the entire Redux state and returns the piece of it that we care about. In our case, that is state.counter.count. Whenever that value changes, our component re-renders with the new number. We never had to call subscribe or store the number in a useState variable of our own.
The useDispatch hook gives us the store's dispatch function. Each button calls it with an action created by increase() or decrease(). We aren't calling the reducers directly. We are describing what happened by dispatching an action, and the store takes it from there.
The aria-label attributes are there because the buttons only contain a plus or minus symbol, and a label helps screen readers announce what each button actually does.
This is where Redux starts to feel useful. Create another file in the src folder called Status.jsx:
import { useSelector } from "react-redux";
function Status() {
const isEven = useSelector((state) => state.counter.count % 2 === 0);
return <p className="status">The count is {isEven ? "even" : "odd"}</p>;
}
export default Status;
This component has no props and no connection to Counter. It simply reaches into the store and asks whether the count is even. Notice that our selector returns a boolean and not the count itself. This is a good habit. useSelector re-renders your component whenever the value you return changes, so by selecting only what you need (true or false), the Status component will only re-render when the answer actually changes. In our little counter, the answer flips on every click, so you won't see a difference. In a larger app where a lot of state changes around it, this habit saves a lot of unnecessary work.
Next, replace the contents of src/App.jsx with the following:
import Counter from "./Counter";
import Status from "./Status";
function App() {
return (
<>
<Counter />
<Status />
</>
);
}
export default App;
Notice that we don't pass any props to Counter or Status. They both get what they need directly from the store.
The last thing is to make our app look nice. Open src/index.css and replace everything in there with the following:
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #8e7c93;
}
#root {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: #fff;
margin: 100px 0 20px;
padding: 10px;
border-radius: 3px;
width: 200px;
display: flex;
align-items: center;
justify-content: space-between;
}
.buttons {
background-color: transparent;
border: none;
font-size: 16px;
font-weight: bold;
border-radius: 3px;
transition: all 0.15s ease-in;
cursor: pointer;
}
.buttons:hover:nth-child(1) {
background-color: #f45b69;
}
.buttons:hover:nth-child(3) {
background-color: #c0dfa1;
}
.status {
color: #fff;
}
At this point, we are done. Save your changes across all of the files and start your dev server by running npm run dev in your terminal. Open the address it prints (usually http://localhost:5173), and you should see your counter working. Click the buttons, and watch the number and the even/odd message update together.
If you have the Redux DevTools browser extension installed, open it while your app is running. Because configureStore turns the DevTools on for you, you will see each counter/increase and counter/decrease action show up in a list as you click, along with the state after each one. That is one of the best reasons to use Redux.
If your project uses TypeScript, there is one more thing worth knowing about. The useSelector and useDispatch hooks don't know what your store looks like, so you would have to annotate the state type each time you use them. The recommended fix is to create pre-typed versions of the hooks once and use those everywhere. Here is what that looks like in a store.ts file:
import { configureStore } from "@reduxjs/toolkit";
import { useDispatch, useSelector } from "react-redux";
import counterReducer from "./counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
From here on, you would import useAppDispatch and useAppSelector in your components instead of the plain hooks, and state.counter.count will be fully typed and autocompleted. We are not going to go down the TypeScript path in this tutorial, but I wanted you to know this exists.
A very common reason people reach for Redux is to store data that comes from a server. You could do that by writing a slice with a bunch of reducers for loading, success, and error states. You don't have to, though. Redux Toolkit includes a tool called RTK Query that is built for exactly this. You describe your API endpoints with createApi, and it gives you hooks that fetch the data, cache it, and keep track of loading and error states for you. You can find it in the @reduxjs/toolkit/query/react module, and the RTK Query documentation is a great place to learn more. Libraries like TanStack Query solve the same problem if you don't want Redux in the picture at all.
Having built something with it, you are in a much better position to judge. For our counter, Redux is overkill. A single useState would have done the job, and if the two components needed to share the number, a little context or useReducer would have been enough. We used a small example so that the Redux parts were easy to see.
Redux starts to pay for itself when the app is big, when lots of unrelated components need the same data, and when you want a clear record of every change that happens to your state. If that's not what you are dealing with, useState, useReducer, and context are the better starting point, and a library like Zustand is a lighter option when you need shared state without all of the structure. You can always add Redux later if the pain shows up.
The big idea in this tutorial is that connecting Redux to React takes very little code. You create a store with Redux Toolkit, wrap your app with Provider, read data with useSelector, and change data by sending actions with useDispatch. Everything else is just writing slices and components.
Like many things in programming, Redux is simply one of many tools you have for accomplishing a task. Not every situation involving data will need it, and sometimes it adds more complexity than it removes. Dan Abramov, one of the creators of Redux, wrote a great article explaining some of the situations when you probably shouldn't use Redux. I highly encourage you to read that to get the full picture.
If Redux feels like more ceremony than your app needs, there is a much smaller option that I mentioned earlier. We will look at it next.
Next tutorial: Simple Global State with Zustand
Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.
Your support keeps this site going! 😇

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.
Hot off the Forums
:: Copyright KIRUPA 2026 //--