Redux Duck Pattern with Thunk - Complete Implementation Guide
A comprehensive guide to implementing Redux Duck pattern with Thunk middleware for handling async operations and state management in React applications.
What is Redux Duck Pattern?​
The Redux Duck pattern is a way to organize Redux code by bundling related actions, action creators, and reducers into a single file. This pattern helps maintain modularity and makes it easier to manage related state logic in one place.
Basic Structure of a Duck File​
A typical Redux Duck file contains:
- Action Types
- Action Creators
- Thunk (Async Action Creators)
- Initial State
- Reducer
Implementation Example​
1. Action Types​
const FETCH_USER_REQUEST = 'user/FETCH_USER_REQUEST';
const FETCH_USER_SUCCESS = 'user/FETCH_USER_SUCCESS';
const FETCH_USER_FAILURE = 'user/FETCH_USER_FAILURE';
- Action types are constants that define the type of action
- Convention:
domain/ACTION_TYPE - Helps prevent typos and enables better debugging
2. Action Creators​
const fetchUserRequest = () => ({
type: FETCH_USER_REQUEST,
});
const fetchUserSuccess = (data) => ({
type: FETCH_USER_SUCCESS,
payload: data,
});
const fetchUserFailure = (error) => ({
type: FETCH_USER_FAILURE,
payload: error,
});
- Pure functions that create action objects
- Each action creator returns an object with
typeand optionalpayload
3. Thunk (Async Action Creator)​
export const fetchUser = () => {
return async (dispatch) => {
dispatch(fetchUserRequest());
try {
const response = await apiCall(); // Your API call here
dispatch(fetchUserSuccess(response.data));
} catch (error) {
dispatch(fetchUserFailure(error.message));
}
};
};
- Thunks are middleware that allow action creators to return functions instead of action objects
- Perfect for handling async operations
- Can dispatch multiple actions during the async operation
4. Initial State​
const initialState = {
loading: false,
data: null,
error: null,
};
- Defines the initial shape of your state
- Should include all possible state properties
5. Reducer​
export default function userReducer(state = initialState, action) {
switch (action.type) {
case FETCH_USER_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_USER_SUCCESS:
return { ...state, loading: false, data: action.payload };
case FETCH_USER_FAILURE:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
- Pure function that takes current state and action, returns new state
- Uses switch statement to handle different action types
- Always returns a new state object (immutability)
Setting Up Redux with Thunk (Example)​
- Install required dependencies:
npm install redux react-redux redux-thunk
- Create your store:
import { createStore, applyMiddleware, combineReducers } from 'redux';
import {thunk} from 'redux-thunk';
import userReducer from './redux/user.duck';
const rootReducer = combineReducers({
user: userReducer,
....,
});
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
- Wrap your app with Provider:
import { Provider } from 'react-redux';
import store from './store.js'
function App() {
return (
<Provider store={store}>
<YourApp />
</Provider>
);
}
- Sample Duck file:
// 1. Action Types
const FETCH_USER_REQUEST = 'user/FETCH_USER_REQUEST';
const FETCH_USER_SUCCESS = 'user/FETCH_USER_SUCCESS';
const FETCH_USER_FAILURE = 'user/FETCH_USER_FAILURE';
// 2. Action Creators
const fetchUserRequest = () => ({
type: FETCH_USER_REQUEST,
});
const fetchUserSuccess = (data) => ({
type: FETCH_USER_SUCCESS,
payload: data,
});
const fetchUserFailure = (error) => ({
type: FETCH_USER_FAILURE,
payload: error,
});
// 3. Thunk (Async Action Creator)
export const fetchUser = () => {
return async (dispatch) => {
dispatch(fetchUserRequest());
try {
const response = await new Promise((resolve) =>
setTimeout(() => {
resolve({
data: {
id: 1,
name: 'Sanju',
message: 'User data loaded successfully!',
},
});
}, 1000)
);
if (response && response.data) {
dispatch(fetchUserSuccess(response.data));
} else {
throw new Error('Invalid response from server');
}
} catch (error) {
dispatch(fetchUserFailure(error.message || 'Unexpected error occurred'));
}
};
};
// 4. Initial State
const initialState = {
loading: false,
data: null,
error: null,
};
// 5. Reducer
export default function userReducer(state = initialState, action) {
switch (action.type) {
case FETCH_USER_REQUEST:
return { ...state,data: null, loading: true, error: null };
case FETCH_USER_SUCCESS:
return { ...state, loading: false, data: action.payload };
case FETCH_USER_FAILURE:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
Using Redux in Components​
import { useDispatch, useSelector } from 'react-redux';
import { fetchUser } from './redux/user.duck';
function UserComponent() {
const dispatch = useDispatch();
const { loading, data, error } = useSelector((state) => state.user);
useEffect(() => {
dispatch(fetchUser());
}, [dispatch]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No user data</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.message}</p>
</div>
);
}
Best Practices​
- Naming Convention: Use
domain/ACTION_TYPEfor action types - Immutable Updates: Always return new state objects
- Error Handling: Include error states in your initial state
- Loading States: Track loading states for better UX
- Action Creators: Keep them pure and simple
- Thunks: Handle async operations and side effects
- Selectors: Use selectors for complex state calculations
Benefits of Duck Pattern​
- Modular and maintainable code
- Easier to understand related logic
- Better code organization
- Simplified testing
- Reduced boilerplate
- Clear separation of concerns
Remember to follow these patterns consistently across your application for better maintainability and scalability.
Using Connect Pattern with mapStateToProps and mapDispatchToProps​
The connect function from react-redux is a higher-order component that connects a React component to the Redux store. It provides two main functions:
1. mapStateToProps​
const mapStateToProps = (state: RootState) => ({
todos: state.todo.todos
});
- Maps Redux state to component props
- Called every time the store state changes
- Returns an object that will be merged with component props
- First parameter is the entire Redux state
- Second parameter (optional) is the component's own props
2. mapDispatchToProps​
// Object shorthand syntax
const mapDispatchToProps = {
addTodo,
toggleTodo,
deleteTodo
};
// OR Function syntax
const mapDispatchToProps = (dispatch) => ({
addTodo: (text) => dispatch(addTodo(text)),
toggleTodo: (id) => dispatch(toggleTodo(id)),
deleteTodo: (id) => dispatch(deleteTodo(id))
});
- Maps dispatch functions to component props
- Can be an object of action creators (shorthand)
- Or a function that returns an object of dispatch functions
- First parameter is the dispatch function
- Second parameter (optional) is the component's own props
Example Component with Connect​
import { connect } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from './todo.duck';
interface TodoPageProps {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
}
const TodoPage: React.FC<TodoPageProps> = ({ todos, addTodo, toggleTodo, deleteTodo }) => {
// Component implementation
};
const mapStateToProps = (state: RootState) => ({
todos: state.todo.todos
});
const mapDispatchToProps = {
addTodo,
toggleTodo,
deleteTodo
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoPage);
Benefits of Connect Pattern​
- Clear separation between presentational and container components
- Automatic re-rendering when connected state changes
- Type safety with TypeScript
- Easy testing of component logic
- Reusable action creators
- Centralized state management
Best Practices for Connect​
- Use TypeScript interfaces for props
- Keep mapStateToProps simple and focused
- Use object shorthand for mapDispatchToProps when possible
- Memoize selectors for complex state transformations
- Split large components into presentational and container components
- Use proper typing for the Redux state