Reducers at a Glance
Reducers are the backbone of state management in Redux. They are pure functions — meaning they have no side effects and always return the same output given the same input — that take an application's current state and an action, then return what the next state should be. In a Redux-managed React app, reducers are the single place where state transitions happen.
The two arguments a reducer receives are straightforward. State is the data your components depend on to render; when it changes, React re-renders. Actions are plain JavaScript objects, the only source of information that tells Redux what happened. Each action carries a type field (and optionally a payload) that the reducer uses to decide how to update the store.
Actions are almost always defined in functions, as shown here:
const action = {
type: 'ADD_TO_CART',
payload: {
product: 'margarine',
quantity: 4
}
}
Here, the action object includes the string type and an arbitrary payload containing the data needed for the update.
State Transitions and Immutability
To watch a reducer work, take a simple counter. Its state is a number, starting at 0, and its two actions, increaseAction and decreaseAction, tell the reducer to bump state up or down. If neither case matches, the reducer returns the current state unchanged.
This brings up an important rule: state must never be mutated in place. Instead, we rely on the spread operator to construct a brand-new object based on the existing state. This preserves the parts of state we aren’t touching while still placing fresh data into it.
The reducer pattern scales easily to more complex data structures, such as lists:
const contactAction = {
type: 'GET_CONTACT',
payload: ['0801234567', '0901234567']
};
const initialState = {
contacts: [],
contact: {},
};
export default function (state = initialState, action) {
switch (action.type) {
case GET_CONTACTS:
return {
...state,
contacts: action.payload,
};
default:
return state;
}
Specifically, by spreading out the old state before merging in the new payload, we ensure that any previous fields stay intact unless we deliberately overwrite them.
Beyond Storing: Middleware in the App
Reducers alone won't handle every need — specifically asynchronous work. If we're fetching remote data, we need extra functionality to enable the store to update asynchronously. Middleware gives us that. For our demo, we apply Redux Thunk via applyMiddleware. Redux has synchronous updates by default, so Thunk is what lets our store process async requests before dispatching the resulting action.
Constructing the overall store combines our reducer with this middleware:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import reducers from './reducers';
const store = createStore(reducers, applyMiddleware(thunk))
ReactDOM.render(
<Provider store={store}>
<>
<App/>
</>
</Provider>,
document.getElementById('root')
)
Making the store available across our component tree is one more step. We wrap the top-level component in a Provider and pass the store to it as a prop, so every child can connect and share data.
Building a Real Redux-Powered Search App
Let's go through a complete, small example: a movie details finder. It uses the Open Movie Database API to retrieve information when a title is submitted through a search bar.
Start with installing the essentials and kicking off the developement server.
create-react-app movie-detail-finder
npm i axios reactstrap react-redux redux redux-thunk
npm start
In our src directory, we structure our components. Our Searchbar component uses connect from react-redux, linking it directly to the store and giving it access to the action that triggers the fetch:
import React from 'react';
import styles from './Searchbar.module.css';
import { connect } from 'react-redux';
import { fetchMovie } from '../../actions';
import Movie from '../Movie/Movie';
class Searchbar extends React.Component{
render(){
return(
<div className={styles.Form}>
<div>
<form onSubmit={this.formHandler}>
<input
type="text"
placeholder="Movie Title"
onChange={e => this.setState({title: e.target.value})}
value={this.state.title}/>
<button type="submit">Search</button>
</form>
</div>
<Movie movie={this.props.movie}/>
</div>
)
}
}
The UI form itself does the heavy lifting. We maintain a title in the component's local state, but for anything global to our app, like retrieved movie details, we rely on Redux’s store. We define an initial local state for the component before wiring up the submit handler that dispatches our action with the typed title.
The fetchMovie action itself is responsible for talking to the API. Success receives data, and our dispatch function hands it over:
import axios from 'axios';
export const fetchMovie = (title) =>
async (dispatch) => {
const response = await
axios.get(
`https://cors-anywhere.herokuapp.com/https://www.omdbapi.com/?t=${title}&apikey=APIKEY`);
dispatch({
type: 'FETCH_MOVIE',
payload: response.data
})
}
That's where the reducer finishes the job, as seen here:
const fetchMovieReducer = (state = null, action) => {
switch(action.type){
case 'FETCH_MOVIE':
return action.payload;
default:
return state;
}
}
const rootReducer = (state, action) => {
return {
movie: fetchMovieReducer(state, action)
}
}
export default rootReducer;
Our fetch reducers contain the logic that returns the retrieved movie details from the action payload, or returns the default state if the action does not match the type of a request we care about. In this case, a root reducer in its own file cleansly bundles the reducers we define.
Wiring Up the UI to Rendering
Our application needs to bring those pieces into a visual interface. The display is divided into the searching form and the presentation component for the retrieved content. The form component asks for a movie, sends that query as the payload of our action, and passes its retrieved result to the Movie component for rendering.
Whether you're delivering a static amount of data or pulling in live feeds, the underlying structure doesn't change: Define actions for discrete behaviors, capture the data retrieved in their payloads, and delegate all decisions about the final state structure to your reducers. Handlers get wired in through connect, keeping your React components and Redux logic clean and separate. In the end, your UI simply reads from your Redux store, always accurately reflecting your newest set of state changes.
Reducers in Practice
Reducer functions receive the current state and an action, then return the next state. Because they are pure functions, the same inputs always produce the same output, and no external values are mutated. The initial call to a reducer uses a default parameter for state, which supplies the starting shape of the store. Inside the reducer, a switch statement over action.type determines which state branch to update, while a default case returns the existing state unchanged for any unrecognized action.
When an action requires new data, it is carried in the action's payload field. The reducer reads that value and produces a fresh state object rather than altering the original. To handle actions that do not alter a particular slice of state, the same state reference is returned, which also helps with performance. For updating nested objects or arrays, you split the change into independent steps so each transformation receives the previous result.
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
id: action.id,
text: action.text,
completed: false
}
]
});
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
});
}
return todo;
})
});
default:
return state;
}
}
Splitting and Combining Reducers
A single reducer handling every action type quickly grows unwieldy for larger applications. The standard approach is to separate concerns by domain: one reducer for todo items, another for the visibility filter, and so on. Each sub-reducer is responsible for only its own slice of the overall state tree. You then combine them with Redux's combineReducers utility, which builds the complete state object from those independent pieces.
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
});
Every sub-reducer receives the whole action, but typically only reacts to action types relevant to its domain. The combined reducer distributes that action downward and reassembles the results. This modular structure makes each piece easier to test in isolation and keeps logic predictable across the store.
State Updates Without Side Effects
Redux discourages storing values that do not truly depend on prior state, like user selections or unique IDs — these are better kept as component state via hooks such as useState. Actions originated from event handlers or form submissions get dispatched through dispatch(), which the connect() function from React-Redux wires into components. For asynchronous workflows, the applyMiddleware function extends dispatch so that different action signatures can be processed before reaching the reducer.
Following this discipline means your application state stays consistent, replayable, and easier to debug, because the store is the single source of truth and every update flows through pure logic.
Resources



