A simple look at what happens when you click a button in React, how data moves from parent to child, and what changes when you add Redux.
It looks simple
You write onClick={() => setCount(count + 1)}. You click the button. The number on the screen changes. It feels fast and simple. But under the hood, React follows the same steps every time. Once you know these steps, React makes a lot more sense.
Here is the example we will look at:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Display count={count} />
</div>
);
}
function Display({ count }) {
console.log("Display rendered");
return <p>Count: {count}</p>;
}
Step 1: the click event
When you click the button, the browser creates a click event. But React does not listen on the button itself. React listens near the top of your app instead. It catches the click there and wraps it in its own event object. This helps React work the same way in every browser.
Step 2: setCount does not update right away
Calling setCount(count + 1) does not change count right away. It just tells React: "use this new value next time you render." React saves this request for later. That is why, right after calling setCount, count still shows the old value. The change has not happened yet. Only a request has been saved.
Step 3: React waits, then re-renders once
If you call setCount many times inside one click handler, React does not re-render many times. React waits, then re-renders only once, using the final value. This saves work and makes your app faster.
After the click handler finishes, React runs the Parent function again. This time, count holds the new value.
Step 4: props move down to the child
When Parent runs again, it creates a new <Display count={count} /> with the new value. React compares this new version to the old one. React sees that Display got a new prop. So Display runs again too. It prints "Display rendered" and shows the new count.
If another component did not get new props, and its own state did not change, React skips it. React does not re-render it. A new prop value is one of the main reasons a child component re-renders.
Step 5: only the changed part updates
React does not rebuild the whole page. It compares the new tree to the old tree. Then it changes only the small part that is different - here, just the text showing the count. Everything else on the page stays the same.
The full sequence
Click
-> Synthetic event fires, onClick handler runs
-> setCount(count + 1) queues a state update
-> React batches updates and schedules a re-render
-> Parent function re-runs with the new count
-> New props passed down to Display
-> React diffs old tree vs new tree
-> Only the changed DOM node is updated on screen
[embedded content: click-to-render sequence - 7 steps]
Each step finishes before the next one starts. This is why state never changes in the middle of a click handler. And it is why props only reach the child after the whole re-render is done.
Parent to child: the shape of the tree
The Display example above is just one step. The same rule repeats at every level of a bigger tree:
[embedded content: props down the component tree]
Each component only gets data from its own parent. Each component only sends data to its own children. A grandchild cannot reach up two levels by itself. The data must pass through every component in between.
Why this matters
Once you see this as a set of steps, not magic, a few confusing things make sense. State does not change right away inside the same function. Many state updates in one handler cause just one re-render. And sending the same prop value again does not make the child do extra work. Props are not shared and changed in place. React makes a fresh value every time it renders, and sends it down again.
Redux: a different setup
Everything above happens inside plain React. State lives inside one component. Props carry it down to the children. Redux changes this. Redux moves state to a new place, and lets it reach much further. Redux uses four main parts: a Slice, a Store, a Provider, and any Component that wants to use it.
Piece 1: the Slice
A slice is simple. It says what the state looks like at the start (initialState). It also says the only ways this state is allowed to change (reducers).
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
createSlice quietly builds two things for you. First, action creators - small functions that describe what just happened. Second, a reducer - the only piece of code allowed to work out the new state.
Piece 2: the Store
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: { counter: counterReducer },
});
Every slice's reducer is added here. Your app has only one store. Think of the store as one big building. Each department (slice) has its own room inside it.
Piece 3: the Provider
import { Provider } from 'react-redux';
import { store } from './store';
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}><App /></Provider>
);
Provider wraps your whole app. It uses React Context to do this. Because of Provider, any component can reach the store. It does not matter how deep the component is. You do not need to pass it down through props by hand. This is the only place Redux still uses the parent-to-child tree from before. After this point, nothing needs that tree anymore.
Piece 4: the Component
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
useSelector reads a value straight from the store. This component re-renders any time that value changes. useDispatch sends an action to the store. The component never changes the state by itself.
[embedded content: action to reducer to UI loop]
No props travel between the button and the other component reading count. Both of them just talk to the same store on their own. This is the real difference from everything in the sections above.
An easy way to remember it
Here is an easy way to remember it. Slice is the menu and the recipe book. Store is the kitchen. Provider is the waiter, carrying food from the kitchen to every table. Component is you - you order food, and you watch your plate arrive.
When this is worth it
This setup is worth it when many, unrelated components need to read or change the same data. It also helps when it gets hard to track "why did this change?" by hand. For a small app, plain props and useState are usually enough. Everything from the earlier sections works just fine on its own.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.