Skip to content

Redux - Integrate React

Redux - Integrating with React using Hooks

Section titled “Redux - Integrating with React using Hooks”

While Redux is a standalone library, it’s most commonly used with UI frameworks like React. The react-redux library provides official bindings to integrate Redux seamlessly with React components, making it easy to access and update the Redux store state from your UI.

Storing application state in Redux, outside of React components, is beneficial when multiple components need access to the same data, or when state needs to persist across component lifecycles without complex prop drilling.

Modern React Redux integration primarily uses two key elements:

  • <Provider>: A component that wraps your entire application (or the parts that need access to the store) and makes the Redux store instance available via React’s context API.
  • Hooks (useSelector, useDispatch): Functions used within your functional components to interact with the Redux store.

The <Provider> component takes the Redux store instance as a prop and should be rendered at the top level of your component tree.

// src/index.js (or main.jsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store'; // Your configured Redux store
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);

Instead of the older connect higher-order component, functional components use hooks to interact with the store:

  • useSelector: Allows your component to extract data from the Redux store state. It takes a selector function as an argument, which receives the entire Redux state object and returns the specific piece of data needed. React Redux automatically subscribes your component to the store, and it will re-render if the data returned by the selector changes.
  • useDispatch: Returns a reference to the dispatch function from the Redux store. You use this to dispatch actions.

Example component using hooks:

// src/features/user/UserProfile.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { logoutUser } from './userSlice'; // Assuming an action creator
function UserProfile() {
// Select user data from the store state
const user = useSelector((state) => state.user.profile);
const dispatch = useDispatch();
if (!user) {
return <div>Please log in.</div>;
}
const handleLogout = () => {
dispatch(logoutUser());
};
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Email: {user.email}</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
export default UserProfile;

Handling Asynchronous Logic (e.g., API Calls)

Section titled “Handling Asynchronous Logic (e.g., API Calls)”

Redux itself is synchronous, but side effects like API calls are common. Redux Toolkit provides createAsyncThunk to handle the typical async request lifecycle (pending, fulfilled, rejected).

createAsyncThunk generates action types for these states and dispatches them automatically based on the promise returned by your payload creator function. You handle these actions in your slice using the extraReducers field.

// src/features/items/itemsSlice.js (continued)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios'; // Example HTTP client
// Define the async thunk
export const fetchItems = createAsyncThunk(
'items/fetchItems', // Action type prefix
async () => { // Payload creator function
const response = await axios.get('http://api.example.com/items');
return response.data; // This becomes the action.payload on fulfillment
}
);
const initialState = {
list: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
const itemsSlice = createSlice({
name: 'items',
initialState,
reducers: {
// ... other synchronous reducers
},
extraReducers(builder) {
builder
.addCase(fetchItems.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchItems.fulfilled, (state, action) => {
state.status = 'succeeded';
state.list = action.payload; // Replace list with fetched data
})
.addCase(fetchItems.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message; // Save error message
});
}
});
export default itemsSlice.reducer;
// --- In your component ---
// import React, { useEffect } from 'react';
// import { useDispatch, useSelector } from 'react-redux';
// import { fetchItems } from './itemsSlice';
//
// function ItemList() {
// const dispatch = useDispatch();
// const items = useSelector((state) => state.items.list);
// const status = useSelector((state) => state.items.status);
//
// useEffect(() => {
// if (status === 'idle') {
// dispatch(fetchItems());
// }
// }, [status, dispatch]);
//
// // ... render items based on status and data
// }

For more complex data fetching and caching needs, consider RTK Query, an advanced data fetching and caching solution built into Redux Toolkit, which can further automate this process. Find it here: RTK Query Overview.

  1. Store: Centralized state container, configured with configureStore (includes DevTools, Thunk).
  2. Provider: Wraps the app, making the store available.
  3. Slice: Defines a piece of state, reducers (with Immer), and action creators using createSlice. Handles async logic via createAsyncThunk and extraReducers.
  4. Component: Uses useSelector to read state and useDispatch to dispatch actions (including async thunks).
  5. Interaction: User action triggers dispatch, which goes to the store, runs the relevant reducer, updates the state, and notifies components via useSelector to re-render.

React Redux, especially with Redux Toolkit and hooks, provides a powerful and maintainable way to manage state in React applications. It encourages a clear separation of concerns and offers performance optimizations by default.