Skip to main content

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:

  1. Action Types
  2. Action Creators
  3. Thunk (Async Action Creators)
  4. Initial State
  5. 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 type and optional payload

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)​

  1. Install required dependencies:
npm install redux react-redux redux-thunk
  1. 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;
  1. Wrap your app with Provider:
import { Provider } from 'react-redux';
import store from './store.js'

function App() {
return (
<Provider store={store}>
<YourApp />
</Provider>
);
}
  1. 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​

  1. Naming Convention: Use domain/ACTION_TYPE for action types
  2. Immutable Updates: Always return new state objects
  3. Error Handling: Include error states in your initial state
  4. Loading States: Track loading states for better UX
  5. Action Creators: Keep them pure and simple
  6. Thunks: Handle async operations and side effects
  7. Selectors: Use selectors for complex state calculations

Benefits of Duck Pattern​

  1. Modular and maintainable code
  2. Easier to understand related logic
  3. Better code organization
  4. Simplified testing
  5. Reduced boilerplate
  6. 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​

  1. Clear separation between presentational and container components
  2. Automatic re-rendering when connected state changes
  3. Type safety with TypeScript
  4. Easy testing of component logic
  5. Reusable action creators
  6. Centralized state management

Best Practices for Connect​

  1. Use TypeScript interfaces for props
  2. Keep mapStateToProps simple and focused
  3. Use object shorthand for mapDispatchToProps when possible
  4. Memoize selectors for complex state transformations
  5. Split large components into presentational and container components
  6. Use proper typing for the Redux state