Education
Developer Advocate
Last updated onSep 6, 2024
Last updated onDec 18, 2023
Redux is a predictable state container for JavaScript apps, often used with libraries such as React to manage the application's state consistently and centrally. At the heart of Redux's architecture are reducers, which are pure functions that take the current state and an action as arguments and return a new state.
A Redux reducer is a function that determines how the state of an application changes in response to an action sent to the store. Think of it as a way to automate state changes, ensuring each transition is predictable and transparent.
Reducers are the only source that can change the state in a Redux application, adhering to the core principles of Redux, where the state is read-only and updates are made immutable. This means that reducers must always create a new object or array rather than modifying the existing state directly.
Reducers play a crucial role in shaping the global state of the application. They handle state transitions based on the action type and payload key provided. The reducer function's return value becomes the new state value, which the Redux store then uses to update the application.
In Redux, the state is a plain JavaScript object, and the reducer is a pure function that takes the previous state and an action and returns the next state. It's important to note that if the reducer receives an undefined state, it must return the application's initial state.
The concept of pure functions is central to Redux reducers. A pure function returns the same output for the same input without causing any side effects. This characteristic is vital for reducers because it ensures predictability and facilitates features like time travel debugging.
Reducers are the building blocks of the Redux state management pattern. They are designed to handle updates to the state object by responding to actions with a specific type key.
Each reducer function is defined to handle a slice of the state value, and it must be a pure function. Given the same input, the function should always produce the same output without any side effects. Reducers typically use a switch statement to respond to different action types and return the new state accordingly.
1function myReducer(state = initialState, action) { 2 switch (action.type) { 3 case 'ACTION_TYPE': 4 // Logic to return new state 5 break; 6 default: 7 return state; 8 } 9} 10
In the above example, myReducer is a reducer function that takes two parameters: state, which is the current state, and action, which is an object that represents what change should happen.
The initial state is the starting point of the reducer function. It represents the state before any actions have been dispatched to the Redux store. Defining an initial state is crucial as it provides the default value for the state parameter the first time the reducer is called.
1const initialState = { 2 count: 0 3}; 4
In this example, the initialState is a plain JavaScript object that sets the initial count to 0.
When an action is dispatched, the reducer's switch statement checks the action type and executes the corresponding case to return a new state. Remembering that reducers must always return a new object or array rather than mutating the existing state directly is important.
1case 'INCREMENT': 2 return { 3 ...state, 4 count: state.count + 1 5 }; 6
Here, the reducer handles an 'INCREMENT' action type by returning a new state object with the count increased by one, using the spread operator to ensure that other state data is unaffected.
While actions and reducers are integral to Redux, they serve different purposes. Actions are plain JavaScript objects that represent an intention to change the state, while reducers are functions that decide how the state changes in response to those actions.
Every action object must have a type key indicating the action being performed. This type key is used by the reducer to determine how to calculate the new state.
1const incrementAction = { 2 type: 'INCREMENT' 3}; 4
In this example, incrementAction has a type key of 'INCREMENT', which the reducer will use to match against its switch statement cases.
Reducer functions use the information from the action, such as the type key and any additional payload, to determine the state transition. They apply the logic based on the action type to produce a new state value.
1case 'DECREMENT': 2 return { 3 ...state, 4 count: state.count - 1 5 }; 6
In this case, the reducer responds to a 'DECREMENT' action by creating a new state with the count decreased by one.
When crafting reducers for a Redux application, certain best practices ensure that the state management remains predictable and maintainable.
To maintain the purity of a reducer function, avoiding any side effects or mutations of the state is essential. Instead, return a new object that represents the updated state.
1case 'ADD_TODO': 2 return { 3 ...state, 4 todos: [...state.todos, action.payload] 5 }; 6
In this reducer case, a new todo item is added to the todos array without mutating the existing array, thanks to the spread operator.
The switch statement is commonly used within reducer functions to handle different action types. It provides a clear, concise way to match action types and return the appropriate new state.
1switch (action.type) { 2 case 'SET_VISIBILITY_FILTER': 3 return { 4 ...state, 5 visibilityFilter: action.payload.filter 6 }; 7 // other cases 8} 9
Here, the reducer updates the visibilityFilter property based on the action's payload.
Managing the entire state within a single reducer becomes impractical as applications grow. Redux provides a way to combine multiple reducers into a single root reducer to handle the global state.
The state tree is the whole state of your application. Each reducer manages its branch of the state tree, and the combineReducers function merges them into a single state object.
1import { combineReducers } from 'redux'; 2import todos from './todos'; 3import visibilityFilter from './visibilityFilter'; 4 5const rootReducer = combineReducers({ 6 todos, 7 visibilityFilter 8}); 9 10export default rootReducer; 11
In this example, combineReducers is used to create a rootReducer that combines the todos and visibilityFilter reducers.
React and Redux can be used together to manage state in a React application. React Redux provides bindings that make connecting Redux reducers to React components easy.
The <Provider>
component from React Redux is used to pass the Redux store to React components.
1import React from 'react'; 2import ReactDOM from 'react-dom'; 3import { Provider } from 'react-redux'; 4import store from './store'; 5import App from './App'; 6 7ReactDOM.render( 8 <Provider store={store}> 9 <App /> 10 </Provider>, 11 document.getElementById('root') 12); 13
In this code snippet, the Provider store is wrapping the App component, making the Redux store available to all components in the application.
Advanced techniques in reducer composition can help manage more complex state logic and enhance the capabilities of Redux.
Redux's design allows for time travel debugging, where developers can step through state changes to debug their applications effectively.
1import { createStore } from 'redux'; 2import rootReducer from './reducers'; 3 4const store = createStore(rootReducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); 5
By integrating the Redux DevTools Extension, developers can access time travel debugging features.
Reducers can be composed and abstracted for complex state logic to handle various scenarios.
1function createFilteredReducer(reducerFunction, reducerPredicate) { 2 return (state, action) => { 3 const isInitializationCall = state === undefined; 4 const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall; 5 return shouldRunWrappedReducer ? reducerFunction(state, action) : state; 6 }; 7} 8
This higher-order reducer runs the wrapped reducer function only if the predicate function returns true for a given action.
Redux has popularized the pattern of reducers, but their utility isn't confined to Redux alone. Reducers can be used where state transitions must be managed predictably.
Yes, reducer functions can be used without Redux. The React useReducer hook, for instance, allows you to manage local component state using the reducer pattern.
1const [state, dispatch] = useReducer(reducer, initialState); 2
This hook provides a way to encapsulate local state management logic in a reducer function, similar to how you would in a Redux application.
With the advent of new state management libraries and built-in hooks like useState and useReducer in React, some might wonder if Redux is becoming obsolete.
Redux is not deprecated; it remains a widely used state management library. However, developers now have more options based on the complexity and requirements of their applications.
As applications grow, how reducers are structured becomes increasingly important to ensure that the codebase remains scalable and maintainable.
Reducers should be split according to the logical domains of the application state. This modular approach, often called "ducks" or "slices," helps organize the code related to specific features or functionality.
1const rootReducer = combineReducers({ 2 user: userReducer, 3 posts: postsReducer, 4 // other reducers 5}); 6
In this structure, each reducer, such as userReducer or postsReducer, manages its part of the global state.
To illustrate the concepts discussed, let's look at practical examples of creating and using reducers in a Redux store.
Consider a simple counter application where the reducer manages the state changes for increment and decrement actions.
1function counterReducer(state = { count: 0 }, action) { 2 switch (action.type) { 3 case 'INCREMENT': 4 return { count: state.count + 1 }; 5 case 'DECREMENT': 6 return { count: state.count - 1 }; 7 default: 8 return state; 9 } 10} 11
This reducer listens for INCREMENT and DECREMENT action types and updates the count accordingly.
Testing is a critical part of developing robust applications. Reducers, being pure functions, are particularly straightforward to test.
Reducer tests should verify that the correct state is returned for given actions and that no side effects occur.
1describe('counterReducer', () => { 2 it('should handle INCREMENT', () => { 3 expect(counterReducer({ count: 0 }, { type: 'INCREMENT' })).toEqual({ count: 1 }); 4 }); 5 // other tests 6}); 7
In this test, we assert that the counterReducer correctly handles an INCREMENT action.
Understanding reducers is key to using Redux effectively, but there are common pitfalls that developers should be aware of.
One common mistake is mutating the state directly in a reducer instead of returning a new state object. Another is confusing the role of actions and reducers, where actions are the messages sent to the store, and reducers are the pure functions that interpret these messages and update the state.
Reducers are a fundamental concept in Redux and have influenced state management patterns outside Redux.
Reducers ensure that state transitions are predictable and maintainable. They are a powerful pattern for managing state in modern web applications, whether used with Redux or other state management solutions.
Redux continues to be a valuable tool in a developer's toolkit, especially for complex state management needs. Its reducer-centric architecture has set a standard for predictably approaching state changes.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.