This episode teaches how components read and change state: useSelector for reading, useDispatch for dispatching actions. You'll understand selectors as pure functions, when re-renders happen with strict equality, and how to use shallowEqual for complex data.

The store and slice exist, but they're not connected to the UI yet. Episode 5 bridges the two: hooks from react-redux that connect React components to the Redux store. This is the moment when the state that's been living inside the store finally shows up on the screen.
We'll cover useSelector for reading state, useDispatch for dispatching actions, the principle of selectors as pure functions, and when and how re-renders happen — including using shallowEqual to prevent excessive re-renders.
useSelector(selector) accepts a selector function and returns the selected piece of state. React Redux automatically subscribes to the store and re-renders the component whenever the selector result changes.
import { useSelector, useDispatch } from "react-redux"
function Counter() {
const count = useSelector((state) => state.counter.count)
const dispatch = useDispatch()
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
)
}useSelector((state) => state.counter.count) reads the count value from the store. useDispatch() returns a ready-to-use dispatch function, and dispatch(increment()) sends an action from the counter slice.
A selector is a pure function: it receives the entire state and returns the selected data. Because it's pure, a selector can be tested, rewritten, and combined easily.
const selectCount = (state) => state.counter.countWriting a selector as a standalone constant allows reuse across many components and forms the basis of createSelector memoization, which we'll cover in episode 13.
React Redux compares the selector result with the previous one using strict equality. The component only re-renders if the value changed:
const name = useSelector((state) => state.user.name)state.user.name is a string — a primitive. Strings are compared by value, so re-renders only happen when the name actually changes.
The problem appears when a selector returns a freshly created object or array every time:
const user = useSelector((state) => ({
name: state.user.name,
age: state.user.age,
})){ name, age } is a new object on every selector evaluation, so the result is always different under strict equality — the component re-renders endlessly. The full solution is in episode 13, but there's a shortcut: shallowEqual below.
The smaller the selected data, the less often re-renders happen. Select only the fields the component actually uses:
const total = useSelector((state) => state.cart.items.reduce((s, i) => s + i.qty, 0))This pattern does trigger a re-render on every cart content change, but it only returns a single number, so cross-component state comparisons stay cheap.
When a component needs several fields at once, combine them with shallowEqual:
import { useSelector, shallowEqual } from "react-redux"
const { name, age } = useSelector(
(state) => ({
name: state.user.name,
age: state.user.age,
}),
shallowEqual,
)shallowEqual compares each top-level field individually rather than comparing object references. If name and age are the same as before, there's no re-render — a direct fix for the new-object problem above.
For many values, a single useSelector call may select several fields at once as long as they're combined with the right comparator:
const status = useSelector((state) => state.posts.status)
const items = useSelector((state) => state.posts.items)Two separate selector calls are also valid and actually easier to predict. Use separate calls for values that rarely change, and shallowEqual when they must be grouped into a single object.
Episode 5 closes the loop on basic state management: components read state with useSelector, change it with useDispatch, and re-render only when the selector result actually changes. Performance is already becoming a concern — and it will be a major theme in episode 13.
Key takeaways:
useSelector(selector) reads state and subscribes to changes.useDispatch() returns a function for dispatching actions.shallowEqual groups several fields without excessive re-renders.In the next episode, episode 6, you'll step into the async world: createAsyncThunk — creating actions that fetch API data, understanding the pending, fulfilled, and rejected lifecycle, managing loading, succeeded, and failed statuses, and aborting requests when a component unmounts.