Redux centralizes your application's state into a single Store, updated only through dispatched actions and pure reducer functions. This lesson covers the core Redux API ā createStore, actions, action creators, and reducers ā before wiring any of it into React.
1The Redux Core
The Redux core library centers on a single object called the Store ā think of it as one giant, tightly protected global variable that holds your entire application's state. Rather than scattering state across many components, Redux consolidates it into this one place.
The Store exposes exactly three core operations you'll use constantly: createStore() to set it up, dispatch() to send it actions describing what happened, and getState() to read its current data.
// Example
console.log("Running React...");Redux Core API
2Creating the Store
Every Redux application begins by calling createStore(), imported from the redux package. You must pass your root reducer function into createStore as its argument ā const store = createStore(rootReducer) ā so the Store knows exactly how to calculate a new state whenever it receives a future action.
Without a reducer, the Store has no logic to fall back on; the reducer is what defines how every possible action transforms the current state into the next one.
import { createStore } from 'redux';
const store = createStore(myReducer);3Actions (The WHAT)
Once a Store exists, you interact with it through Actions ā plain JavaScript objects that describe what happened, such as { type: 'USER_LOGIN_SUCCESS' }. The only requirement Redux strictly enforces is that every action object must include a type property, conventionally an uppercase string with underscores.
Actions don't perform any logic themselves; they're just descriptive messages that get sent to the reducer, which decides how to respond to them.
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};Action Signatures
4Action Payloads
Actions often need to carry data along with the type, such as the text a user typed or the id of an item being added. By convention, that extra data goes inside a property named payload, e.g. { type: 'ADD_ITEM', payload: { id: 1, name: 'Apple' } }.
Redux itself doesn't require a payload field ā only type is mandatory ā but using payload consistently is the universal community standard that keeps action shapes predictable across a codebase.
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: text
};
}Data Payloads
5Action Creators
Hardcoding action objects directly inside UI components is risky ā a single typo in the type string, like 'ADD_ITM' instead of 'ADD_ITEM', causes a silent failure with no error thrown. Action creators solve this: an action creator is just a plain JavaScript function that returns a correctly-shaped action object, e.g. function addItem(item) { return { type: 'ADD_ITEM', payload: item }; }.
Calling dispatch(addItem(item)) instead of hand-writing the object everywhere centralizes the action's shape in one place, so a typo only needs fixing once.
function reducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
}Typo Prevention
Wrap objects in functions.
6Reducers (The HOW)
The reducer is the brain of the Store ā a plain function that takes two parameters, the current state and the action that was dispatched, and returns a new value that becomes the next state. On Redux's very first run, state is undefined, which is why reducers always declare a default parameter, like function reducer(state = initialState, action) { ... }.
That default is what seeds the Store with its starting values before any action has ever been dispatched.
// Bad ā: state.val = 1;
// Good ā
: return { ...state, val: 1 };7Switch Statements
A single reducer often has to handle dozens of distinct action types, like 'LOGIN', 'LOGOUT', or 'UPDATE_PROFILE', and chaining if/else blocks for all of them quickly turns unreadable. The industry-standard pattern instead uses a JavaScript switch statement on action.type, with one case per action and a mandatory default: return state; fallback.
That default case matters ā if a reducer doesn't recognize the incoming action type, it must still return the existing state unchanged, never undefined or null.
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState());Switch Statements
8The Immutability Rule
Reducers must be pure functions, which means they can never mutate the state object directly ā writing state.score = state.score + 10; return state; breaks Redux's ability to detect that anything changed. Instead, you use the spread operator (...) to copy every existing property into a brand-new object and then overwrite just the properties that changed: return { ...state, score: state.score + 10 };.
Returning a new object reference is exactly what lets Redux, and React-Redux, efficiently detect that the state actually changed.
<h1>Redux Core Master Unlocked!</h1>Immutability š
{ ...state, changed: new }
9Step-by-Step Breakdown
The Redux Core. Welcome to the Redux Core. Now that we understand the philosophy, let's look at the actual code. To implement Redux, we use the core redux library. The central piece of the architecture is the 'Store'. It acts like a giant, highly protected global variable that holds your entire application state.
Creating the Store. The first step in any Redux application is to create the Store using createStore(). You must pass your root reducer function into createStore as its primary argument, so the Store knows how to handle future actions and calculate new states.
Actions (The WHAT). Once the store exists, you interact with it via Actions. As we covered, an Action is a plain JS object describing WHAT happened. The only strict requirement enforced by Redux is that the object MUST have a type property (typically a string in uppercase with underscores).
Action Payloads. Actions usually need to carry data to the Reducer. If the user typed a message, the action must carry that text string. By convention, we put this extra data inside a property named payload. This isn't technically required by the library, but it is the universal community standard.
Which property is STRICTLY REQUIRED by the Redux core library inside every Action object?
- ādata
- āpayload
- ātype
Action Creators. Hardcoding action objects throughout your UI components is dangerous because a simple typo in the type string will cause silent failures. To fix this, we use 'Action Creators'. An Action Creator is simply a JavaScript function that returns an action object.
Reducers (The HOW). The Reducer is the brain of the Store. It is a standard function that takes two parameters: state (the current data) and action (the event). It returns a new object that replaces the current state. The first time Redux runs, the state is undefined, so you must provide a default initial state parameter (e.g., state = 0).
Switch Statements. Because a reducer might need to handle dozens of different action types ('LOGIN', 'LOGOUT', 'UPDATE_PROFILE', etc.), using if/else blocks gets extremely messy. The industry standard is to use a JavaScript switch statement on action.type.
What must a Reducer ALWAYS return in its default switch case if it does not recognize the action.type?
- āreturn null;
- āreturn state;
- āthrow Error();
The Immutability Rule. As discussed in the intro, reducers MUST be pure functions. They cannot mutate state directly. You must use the spread operator (...) to copy the old state properties into a new object, and then overwrite the specific properties that changed.
Store Dispatch. To execute an action, we use store.dispatch(). You pass an action object (usually generated by an Action Creator) directly into this method. Redux immediately sends this action, along with the current state, into your Root Reducer. It evaluates the switch statement, returns the new state, and saves it in the Store.
Which function do we use to prevent typing errors by generating our Action objects automatically?
- āAction Creators
- āReducers
Reading State. If you need to manually inspect the current state of the global store outside of a React component, you can call store.getState(). This returns the entire massive JavaScript object representing your application's current reality.
Unidirectional Flow. The reason we write all this boilerplate code is to enforce a strict, predictable 'Unidirectional Data Flow'. The UI triggers an Action Creator -> dispatch() sends the Action -> The Reducer calculates a new state -> The Store updates and notifies the UI. Data only ever flows in ONE direction.
Mastery Achieved. Fantastic! You've mastered the Redux Core API. You know how to create the Store, dispatch Actions via Action Creators, and write Pure Reducers using Switch statements. While you can use Redux entirely on its own, it's usually integrated directly into React components.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Redux State Changes Should Still Trigger Accessible UI Updates
When a dispatched action changes state that affects visible feedback, like a form validation error or a loading status, make sure the connected component announces that change via an `aria-live` region ā Redux itself has no awareness of accessibility, so the responsibility falls entirely on the connected UI layer.
2Keep Redux-Driven Focus Management Intentional
If dispatching an action causes a modal or route to open, move keyboard focus into the new content explicitly ā a Store update that changes what's rendered doesn't automatically move focus for screen reader or keyboard users.
SEO Implications
- 1
Redux State Doesn't Exist Server-Side by Default
A plain Redux store initialized with `createStore()` on the client starts fresh with its initial state during server-side rendering; any content computed from client-dispatched actions won't appear in the pre-hydration HTML a crawler sees unless the store is explicitly hydrated with server data.
- 2
Centralizing Content State in Redux Simplifies Consistent Rendering
Keeping page content driven by a single, predictable Store makes it easier to guarantee the same data renders identically wherever it's displayed, reducing the risk of inconsistent or duplicate content across routes.
Best Practices
Always Provide a Default Parameter for the Reducer's State
Since Redux calls every reducer once with `state` as `undefined` to obtain the initial state, always write `function reducer(state = initialState, action)` ā omitting the default causes the Store to start with undefined state.
Never Mutate State Inside a Reducer
Always build a new object or array with the spread operator, like `{ ...state, key: newValue }`, instead of assigning directly to a property on `state` ā mutating in place means Redux and connected components can't detect that anything changed.
Frequent Bugs
A reducer's default case is missing, so dispatching an unrecognized action type causes the state to become undefined.
Always include a `default: return state;` case in the reducer's switch statement so unrecognized action types leave the existing state untouched.
A typo in an action's type string, such as 'ADD_ITM' instead of 'ADD_ITEM', causes the dispatch to silently do nothing.
Use action creator functions instead of hand-writing action objects inline ā centralizing the type string in one function means a typo only needs to be fixed in a single place, and mismatches surface faster.
Real-World Examples
A Counter Reducer With Switch Statement and Default Case
A simple counter feature dispatches `{ type: 'INCREMENT' }` and `{ type: 'DECREMENT' }` actions, and the root reducer uses a switch statement to compute the next numeric state, always falling back to the current state for unrecognized actions.
function counterReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}