Introduction to Redux with React
Redux is a powerful state management library, but its true potential shines when integrated with React applications. Think of Redux as a central bank for your application's state - instead of each component managing its own money (state), they all deposit and withdraw from one secure location.
The React-Redux library serves as the bridge between Redux and React, providing hooks and components that make integration seamless and efficient.
Why Use Redux with React?
- Centralized State Management: All application state lives in one place
- Predictable State Updates: State changes follow a strict unidirectional flow
- Powerful Debugging: Time-travel debugging and action tracking
- Middleware Support: Easy integration of async logic and side effects
- Performance Optimizations: Fine-grained control over component re-rendering
Integration Steps Overview
Integrating Redux into a React application follows a systematic approach:
- Set Up Redux: Install the required packages
- Define Actions: Create action types and action creators
- Create Reducers: Implement reducer functions to handle state updates
- Configure the Store: Set up the Redux store with your reducers
- Provide the Store: Wrap your application with the Redux Provider
- Connect Components: Use React-Redux hooks to access and update state
Restaurant Analogy
Think of Redux integration like setting up a restaurant:
- The Store is your restaurant building
- Actions are customer orders
- Reducers are the kitchen staff preparing dishes based on orders
- The Provider is the restaurant's infrastructure (plumbing, electricity, etc.)
- useSelector is how waiters check if orders are ready
- useDispatch is how waiters submit new orders to the kitchen
Setting Up Redux
Before you can use Redux with React, you need to install the necessary packages:
Installing Redux Packages
npm install redux react-redux
or using Yarn:
yarn add redux react-redux
The redux package is the core Redux library, while react-redux provides the integration with React components.
Additional Packages: For larger applications, you might also want to consider:
redux-thunkorredux-sagafor handling asynchronous actions@reduxjs/toolkitfor simplified Redux developmentredux-devtools-extensionfor enhanced debugging capabilities
Defining Your Store
After setting up your actions and reducers (as covered in previous tutorials), you'll need to create a Redux store. Typically, this is done in a separate file, often named store.js:
Creating a Store
// store.js
import { createStore, combineReducers } from 'redux';
import fruitReducer from './reducers/fruitReducer';
import farmersReducer from './reducers/farmersReducer';
// Combine multiple reducers
const rootReducer = combineReducers({
fruitState: fruitReducer,
farmerState: farmersReducer
});
// Create and configure the store
const configureStore = () => {
return createStore(
rootReducer,
// Add Redux DevTools Extension support
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
};
export default configureStore;
This example creates a store with two slices of state: fruitState and farmerState, each managed by its own reducer. The configureStore function is a factory function that creates and returns a configured Redux store.
The Provider Component
The Provider component from React-Redux is the glue that connects your Redux store to your React application. It uses React's Context API behind the scenes to make the Redux store available to any component in the application that needs it.
Wrapping Your App with Provider
// index.js or main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import configureStore from './store';
import App from './App';
// Create the Redux store
const store = configureStore();
// Render the application wrapped in the Provider
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Context Analogy
The Provider is like a public announcement system in a building. Instead of personally telling each person (component) about an event (state change), you can make a building-wide announcement that anyone who's listening can hear.
By wrapping your entire <App/> component with the Provider, you ensure that any component in your application tree can access the Redux store if needed, without passing props down through multiple layers of components.
Accessing State with useSelector
The useSelector hook is how your React components access data from the Redux store. It accepts a selector function that extracts the data your component needs from the Redux state.
Basic useSelector Usage
import React from 'react';
import { useSelector } from 'react-redux';
function FruitsList() {
// Extract fruits from Redux state
const fruits = useSelector((state) => state.fruitState);
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit.id}>{fruit.name}</li>
))}
</ul>
);
}
The selector function passed to useSelector will be called whenever the Redux store updates. If the selector returns a different value than the previous render, your component will re-render with the new data.
Performance Tip: The useSelector hook performs a shallow equality check on the returned values. If your selector returns a new object or array on each call (even with the same data), it could cause unnecessary re-renders. Consider using memoization with reselect library for complex selectors.
Creating Reusable Selectors
For better code organization and reusability, you can define selector functions separately and import them where needed:
Reusable Selectors
// selectors.js or within your reducer files
export const selectFruits = (state) => state.fruitState;
export const selectFarmerById = (state, farmerId) =>
state.farmerState[farmerId];
// Component.js
import { selectFruits } from '../store/selectors';
function FruitsList() {
const fruits = useSelector(selectFruits);
// Rest of component...
}
This approach makes your components cleaner and allows you to reuse the same selectors across multiple components.
Dispatching Actions with useDispatch
To update the Redux store from your React components, you need to dispatch actions. The useDispatch hook gives your components access to the Redux store's dispatch method.
Basic useDispatch Usage
import React from 'react';
import { useDispatch } from 'react-redux';
import { addFruit } from '../store/actions';
function AddFruitButton() {
const dispatch = useDispatch();
const handleClick = () => {
dispatch(addFruit({ id: Date.now(), name: 'Apple' }));
};
return (
<button onClick={handleClick}>
Add Apple
</button>
);
}
The useDispatch hook returns the dispatch function from the Redux store. When you call dispatch with an action object, Redux forwards that action to the appropriate reducer, which then updates the state.
Dispatching Actions in Event Handlers
The most common place to dispatch actions is in event handlers, such as button clicks, form submissions, or other user interactions:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addFruit } from '../store/actions';
function AddFruitForm() {
const [fruitName, setFruitName] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!fruitName.trim()) return;
dispatch(addFruit({
id: Date.now(),
name: fruitName
}));
setFruitName(''); // Clear the input
};
return (
<form onSubmit={handleSubmit}>
<input
value={fruitName}
onChange={(e) => setFruitName(e.target.value)}
placeholder="Enter fruit name"
/>
<button type="submit">Add Fruit</button>
</form>
);
}
Dispatching Actions in useEffect
Another common pattern is dispatching actions when a component mounts or when certain dependencies change, using the useEffect hook:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchFruits } from '../store/actions';
function FruitsList() {
const dispatch = useDispatch();
const fruits = useSelector(state => state.fruitState);
useEffect(() => {
// Fetch fruits when component mounts
dispatch(fetchFruits());
}, [dispatch]); // dispatch is stable and doesn't need to be in deps array
return (
<ul>
{fruits.map(fruit => (
<li key={fruit.id}>{fruit.name}</li>
))}
</ul>
);
}
Note on Dependencies: Although dispatch is included in the dependency array in the example above, the dispatch function is guaranteed to be stable across renders by React-Redux. This means the effect will only run once when the component mounts, not on subsequent renders.
Practical Example: Fruit Management Application
Let's put everything together in a complete example of a fruit management application using Redux with React:
Step 1: Define Action Types and Creators
// src/store/actions.js
// Action Types
export const ADD_FRUIT = 'ADD_FRUIT';
export const REMOVE_FRUIT = 'REMOVE_FRUIT';
export const FETCH_FRUITS = 'FETCH_FRUITS';
// Action Creators
export const addFruit = (fruit) => ({
type: ADD_FRUIT,
fruit
});
export const removeFruit = (fruitId) => ({
type: REMOVE_FRUIT,
fruitId
});
export const fetchFruits = () => ({
type: FETCH_FRUITS
});
Step 2: Create the Reducer
// src/store/fruitReducer.js
import { ADD_FRUIT, REMOVE_FRUIT, FETCH_FRUITS } from './actions';
// Initial state
const initialState = [];
// Sample data for the FETCH_FRUITS action
const sampleFruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' }
];
// Reducer function
const fruitReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_FRUIT:
return [...state, action.fruit];
case REMOVE_FRUIT:
return state.filter(fruit => fruit.id !== action.fruitId);
case FETCH_FRUITS:
return [...sampleFruits]; // In a real app, this would come from an API
default:
return state;
}
};
// Selectors
export const selectFruits = (state) => state.fruitState;
export const selectFruitById = (state, id) =>
state.fruitState.find(fruit => fruit.id === id);
export default fruitReducer;
Step 3: Configure the Store
// src/store/index.js
import { createStore } from 'redux';
import { combineReducers } from 'redux';
import fruitReducer from './fruitReducer';
// Root reducer
const rootReducer = combineReducers({
fruitState: fruitReducer
// Add more reducers here as your app grows
});
// Store factory function
const configureStore = () => {
return createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
};
export default configureStore;
Step 4: Provide the Store to React
// src/index.js or src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import configureStore from './store';
import App from './App';
import './index.css';
const store = configureStore();
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Step 5: Create Components
// src/components/FruitsList.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchFruits, removeFruit } from '../store/actions';
import { selectFruits } from '../store/fruitReducer';
function FruitsList() {
const dispatch = useDispatch();
const fruits = useSelector(selectFruits);
useEffect(() => {
dispatch(fetchFruits());
}, [dispatch]);
const handleRemove = (id) => {
dispatch(removeFruit(id));
};
if (fruits.length === 0) {
return <p>No fruits available. Add some fruits!</p>;
}
return (
<div className="fruits-list">
<h2>Available Fruits</h2>
<ul>
{fruits.map(fruit => (
<li key={fruit.id}>
{fruit.name}
<button
onClick={() => handleRemove(fruit.id)}
className="remove-button"
>
Remove
</button>
</li>
))}
</ul>
</div>
);
}
// src/components/AddFruitForm.jsx
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addFruit } from '../store/actions';
function AddFruitForm() {
const [name, setName] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!name.trim()) return;
const newFruit = {
id: Date.now(),
name: name.trim()
};
dispatch(addFruit(newFruit));
setName('');
};
return (
<div className="add-fruit-form">
<h2>Add New Fruit</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter fruit name"
required
/>
<button type="submit">Add Fruit</button>
</form>
</div>
);
}
// src/App.jsx
import React from 'react';
import FruitsList from './components/FruitsList';
import AddFruitForm from './components/AddFruitForm';
import './App.css';
function App() {
return (
<div className="app">
<h1>Fruit Management App</h1>
<div className="app-container">
<AddFruitForm />
<FruitsList />
</div>
</div>
);
}
export default App;
This complete example demonstrates how to:
- Set up Redux with actions, reducers, and a store
- Connect Redux to React using the Provider component
- Use useSelector to access Redux state in components
- Use useDispatch to update Redux state from components
- Implement common patterns like fetching data on component mount
Best Practices and Common Patterns
Organizing Redux Logic
As your application grows, it's important to organize your Redux code effectively. Here are some common approaches:
Feature-Based Organization
src/
features/
fruits/
fruitActions.js
fruitReducer.js
fruitSelectors.js
farmers/
farmerActions.js
farmerReducer.js
farmerSelectors.js
store.js
Ducks Pattern
The "Ducks" pattern combines actions, reducer, and selectors in a single file:
// src/features/fruits.js
// Action Types
const ADD_FRUIT = 'app/fruits/ADD_FRUIT';
const REMOVE_FRUIT = 'app/fruits/REMOVE_FRUIT';
// Action Creators
export const addFruit = (fruit) => ({ type: ADD_FRUIT, fruit });
export const removeFruit = (id) => ({ type: REMOVE_FRUIT, id });
// Selectors
export const selectFruits = (state) => state.fruits;
// Reducer
const initialState = [];
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADD_FRUIT:
return [...state, action.fruit];
case REMOVE_FRUIT:
return state.filter(fruit => fruit.id !== action.id);
default:
return state;
}
}
Performance Optimizations
Here are some tips to optimize performance when using Redux with React:
- Memoize Selectors: Use
reselectlibrary for expensive computations - Normalize State: Structure complex state as normalized objects instead of nested arrays
- Selective Rendering: Select only the specific data your component needs
- Avoid New References: Be careful not to create new object/array references if the data hasn't changed
Memoized Selector Example
import { createSelector } from 'reselect';
// Basic selectors
const selectFruits = state => state.fruits;
const selectFilter = state => state.filter;
// Memoized selector
const selectVisibleFruits = createSelector(
[selectFruits, selectFilter],
(fruits, filter) => {
switch (filter) {
case 'SHOW_ALL':
return fruits;
case 'SHOW_RED_FRUITS':
return fruits.filter(fruit => fruit.color === 'red');
default:
return fruits;
}
}
);
Handling Asynchronous Operations
Real-world applications often need to perform asynchronous operations like API calls. Redux itself is synchronous, but middleware like Redux Thunk or Redux Saga can handle async logic.
Redux Thunk Example
// Action Types
const FETCH_FRUITS_REQUEST = 'FETCH_FRUITS_REQUEST';
const FETCH_FRUITS_SUCCESS = 'FETCH_FRUITS_SUCCESS';
const FETCH_FRUITS_FAILURE = 'FETCH_FRUITS_FAILURE';
// Action Creators
const fetchFruitsRequest = () => ({ type: FETCH_FRUITS_REQUEST });
const fetchFruitsSuccess = (fruits) => ({ type: FETCH_FRUITS_SUCCESS, fruits });
const fetchFruitsFailure = (error) => ({ type: FETCH_FRUITS_FAILURE, error });
// Thunk Action Creator
export const fetchFruits = () => {
return async (dispatch) => {
dispatch(fetchFruitsRequest());
try {
const response = await fetch('/api/fruits');
const data = await response.json();
dispatch(fetchFruitsSuccess(data));
} catch (error) {
dispatch(fetchFruitsFailure(error.toString()));
}
};
};
To use Redux Thunk, you need to add it as middleware when creating your store:
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
Redux DevTools
Redux DevTools is an essential browser extension for debugging Redux applications. It allows you to:
- Inspect every action dispatched
- See the state before and after each action
- Time-travel to previous states
- Import and export application state
Setting Up Redux DevTools
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk))
);
To use Redux DevTools, you need to install the browser extension:
Redux DevTools Extension
Summary and Key Points
What You've Learned
- How to integrate Redux into a React application
- The role of the
Providercomponent in connecting Redux to React - Using
useSelectorto access Redux state in function components - Using
useDispatchto update Redux state from function components - Common patterns and best practices for Redux with React
Next Steps
- Learn about
Redux Toolkitfor simplified Redux development - Explore middleware for handling side effects (Redux-Thunk, Redux-Saga)
- Study normalized state structures for complex applications
- Learn how to test Redux applications
- Discover advanced patterns like Redux with TypeScript
Challenge Exercise
Extend the fruit management application from this tutorial with the following features:
- Add a fruit category feature (fruits belong to categories like "citrus", "tropical", etc.)
- Implement filtering fruits by category
- Add a search functionality to find fruits by name
- Implement fruit inventory tracking (count of each fruit)
- Add persistent storage using localStorage
This exercise will help solidify your understanding of Redux with React and introduce some more advanced patterns.