Understanding Redux Actions
Actions are the only way to send data from your application to your Redux store. They are plain JavaScript objects that must have a `type` property indicating the type of action being performed.
📨 The Mail System Analogy
Think of Redux actions as mail:
- Action: A letter with specific instructions
- Type: The address/department it's going to
- Payload: The contents of the letter
- Action Creator: The person/form that writes the letter
- Dispatch: Dropping the letter in the mailbox
Just as mail must follow a standard format to be delivered, actions must follow Redux conventions to be processed correctly.
Action Structure
graph TD
A[Action Object] --> B[type: string required]
A --> C[payload: any optional]
A --> D[meta: object optional]
A --> E[error: boolean optional]
style B fill:#f96
style C fill:#9cf
style D fill:#9f9
style E fill:#ff9
Flux Standard Action (FSA)
Redux recommends following the Flux Standard Action convention for consistency:
// Basic action
{
type: 'ADD_TODO',
payload: {
id: 1,
text: 'Learn Redux Actions'
}
}
// Error action
{
type: 'FETCH_USER_FAILURE',
payload: new Error('User not found'),
error: true
}
// Action with metadata
{
type: 'TRACK_EVENT',
payload: {
eventName: 'button_click'
},
meta: {
analytics: true,
timestamp: Date.now()
}
}
Action Types
Naming Conventions
Action types should be:
- Descriptive and specific
- Written in CONSTANT_CASE or domain/eventName format
- Reflect what happened, not what should happen
// Constants for action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
// Modern Redux Toolkit style
const FETCH_POSTS_PENDING = 'posts/fetchPending';
const FETCH_POSTS_FULFILLED = 'posts/fetchFulfilled';
const FETCH_POSTS_REJECTED = 'posts/fetchRejected';
// Domain-based organization
const USER_LOGGED_IN = 'user/loggedIn';
const USER_LOGGED_OUT = 'user/loggedOut';
const USER_PROFILE_UPDATED = 'user/profileUpdated';
// Feature-based organization
export const todoActionTypes = {
ADD: 'todos/add',
TOGGLE: 'todos/toggle',
REMOVE: 'todos/remove',
UPDATE: 'todos/update',
CLEAR_COMPLETED: 'todos/clearCompleted'
};
Action Creators
Action creators are functions that create and return action objects. They make actions reusable and help avoid typos.
// Simple action creator
const addTodo = (text) => {
return {
type: 'ADD_TODO',
payload: {
id: Date.now(),
text,
completed: false
}
};
};
// Arrow function shorthand
const toggleTodo = id => ({
type: 'TOGGLE_TODO',
payload: id
});
// Action creator with validation
const updateTodo = (id, updates) => {
if (!id) {
throw new Error('ID is required to update a todo');
}
return {
type: 'UPDATE_TODO',
payload: { id, updates }
};
};
// Action creator with prepared payload
const addUser = (name, email) => {
return {
type: 'ADD_USER',
payload: {
id: uuidv4(),
name,
email,
createdAt: new Date().toISOString()
}
};
};
// Usage
dispatch(addTodo('Learn Redux'));
dispatch(toggleTodo(123));
dispatch(updateTodo(123, { text: 'Master Redux' }));
Async Action Patterns
sequenceDiagram
participant UI
participant ActionCreator
participant Middleware
participant Reducer
participant Store
UI->>ActionCreator: Call async action
ActionCreator->>Middleware: Dispatch START action
Middleware->>Reducer: Process START
Reducer->>Store: Update loading state
ActionCreator->>ActionCreator: Perform async operation
alt Success
ActionCreator->>Middleware: Dispatch SUCCESS action
Middleware->>Reducer: Process SUCCESS
Reducer->>Store: Update with data
else Failure
ActionCreator->>Middleware: Dispatch FAILURE action
Middleware->>Reducer: Process FAILURE
Reducer->>Store: Update with error
end
Async Action Creator Pattern
// Traditional thunk pattern
const fetchUser = (userId) => {
return async (dispatch) => {
dispatch({ type: 'FETCH_USER_START' });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
dispatch({
type: 'FETCH_USER_SUCCESS',
payload: user
});
} catch (error) {
dispatch({
type: 'FETCH_USER_FAILURE',
payload: error.message,
error: true
});
}
};
};
// Action creators for each phase
const fetchUserStart = () => ({
type: 'FETCH_USER_START'
});
const fetchUserSuccess = (user) => ({
type: 'FETCH_USER_SUCCESS',
payload: user
});
const fetchUserFailure = (error) => ({
type: 'FETCH_USER_FAILURE',
payload: error,
error: true
});
// Async action creator using the above
const fetchUserData = (userId) => async (dispatch) => {
dispatch(fetchUserStart());
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Network response was not ok');
const user = await response.json();
dispatch(fetchUserSuccess(user));
} catch (error) {
dispatch(fetchUserFailure(error.message));
}
};
Advanced Action Creator Patterns
Action Creator Factory
// Factory for creating CRUD action creators
const createCrudActionCreators = (resourceName) => {
const types = {
CREATE: `${resourceName}/create`,
READ: `${resourceName}/read`,
UPDATE: `${resourceName}/update`,
DELETE: `${resourceName}/delete`,
LIST: `${resourceName}/list`
};
return {
create: (data) => ({
type: types.CREATE,
payload: data
}),
read: (id) => ({
type: types.READ,
payload: id
}),
update: (id, data) => ({
type: types.UPDATE,
payload: { id, data }
}),
delete: (id) => ({
type: types.DELETE,
payload: id
}),
list: (filters = {}) => ({
type: types.LIST,
payload: filters
})
};
};
// Usage
const userActions = createCrudActionCreators('users');
const postActions = createCrudActionCreators('posts');
dispatch(userActions.create({ name: 'John' }));
dispatch(postActions.list({ limit: 10 }));
Bound Action Creators
// Binding action creators to dispatch
import { bindActionCreators } from 'redux';
const todoActionCreators = {
addTodo,
toggleTodo,
removeTodo
};
// In a React component
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(todoActionCreators, dispatch);
};
// Or manually
const boundAddTodo = (text) => dispatch(addTodo(text));
const boundToggleTodo = (id) => dispatch(toggleTodo(id));
// Usage in component
props.addTodo('New todo');
props.toggleTodo(123);
Real-World Example: E-commerce Actions
// E-commerce action types
const actionTypes = {
// Product actions
FETCH_PRODUCTS_REQUEST: 'products/fetchRequest',
FETCH_PRODUCTS_SUCCESS: 'products/fetchSuccess',
FETCH_PRODUCTS_FAILURE: 'products/fetchFailure',
// Cart actions
ADD_TO_CART: 'cart/addItem',
REMOVE_FROM_CART: 'cart/removeItem',
UPDATE_CART_QUANTITY: 'cart/updateQuantity',
CLEAR_CART: 'cart/clear',
// Checkout actions
CHECKOUT_REQUEST: 'checkout/request',
CHECKOUT_SUCCESS: 'checkout/success',
CHECKOUT_FAILURE: 'checkout/failure',
// User actions
LOGIN_REQUEST: 'user/loginRequest',
LOGIN_SUCCESS: 'user/loginSuccess',
LOGIN_FAILURE: 'user/loginFailure',
LOGOUT: 'user/logout'
};
// Product action creators
const productActions = {
fetchProducts: (category = null) => async (dispatch) => {
dispatch({ type: actionTypes.FETCH_PRODUCTS_REQUEST });
try {
const url = category
? `/api/products?category=${category}`
: '/api/products';
const response = await fetch(url);
const products = await response.json();
dispatch({
type: actionTypes.FETCH_PRODUCTS_SUCCESS,
payload: products
});
} catch (error) {
dispatch({
type: actionTypes.FETCH_PRODUCTS_FAILURE,
payload: error.message,
error: true
});
}
}
};
// Cart action creators
const cartActions = {
addToCart: (product, quantity = 1) => ({
type: actionTypes.ADD_TO_CART,
payload: {
product,
quantity,
addedAt: new Date().toISOString()
}
}),
removeFromCart: (productId) => ({
type: actionTypes.REMOVE_FROM_CART,
payload: productId
}),
updateQuantity: (productId, quantity) => {
if (quantity < 0) {
throw new Error('Quantity cannot be negative');
}
return {
type: actionTypes.UPDATE_CART_QUANTITY,
payload: { productId, quantity }
};
},
clearCart: () => ({
type: actionTypes.CLEAR_CART
})
};
// Checkout action creators
const checkoutActions = {
processCheckout: (cartItems, paymentInfo) => async (dispatch) => {
dispatch({ type: actionTypes.CHECKOUT_REQUEST });
try {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cartItems, paymentInfo })
});
if (!response.ok) {
throw new Error('Checkout failed');
}
const order = await response.json();
dispatch({
type: actionTypes.CHECKOUT_SUCCESS,
payload: order
});
// Clear cart after successful checkout
dispatch(cartActions.clearCart());
return order;
} catch (error) {
dispatch({
type: actionTypes.CHECKOUT_FAILURE,
payload: error.message,
error: true
});
throw error; // Re-throw to handle in UI
}
}
};
// Usage in components
const ProductList = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(productActions.fetchProducts());
}, [dispatch]);
const handleAddToCart = (product) => {
dispatch(cartActions.addToCart(product));
};
// ... rest of component
};
Testing Action Creators
// Testing simple action creators
describe('Todo Action Creators', () => {
it('should create an action to add a todo', () => {
const text = 'Finish documentation';
const expectedAction = {
type: 'ADD_TODO',
payload: expect.objectContaining({
text,
completed: false
})
};
expect(addTodo(text)).toEqual(expectedAction);
});
it('should create an action to toggle a todo', () => {
const id = 123;
const expectedAction = {
type: 'TOGGLE_TODO',
payload: id
};
expect(toggleTodo(id)).toEqual(expectedAction);
});
});
// Testing async action creators
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('Async Action Creators', () => {
afterEach(() => {
fetchMock.restore();
});
it('creates FETCH_USER_SUCCESS when fetching user has been done', () => {
fetchMock.getOnce('/api/users/1', {
body: { id: 1, name: 'John Doe' },
headers: { 'content-type': 'application/json' }
});
const expectedActions = [
{ type: 'FETCH_USER_START' },
{
type: 'FETCH_USER_SUCCESS',
payload: { id: 1, name: 'John Doe' }
}
];
const store = mockStore({ users: {} });
return store.dispatch(fetchUser(1)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('creates FETCH_USER_FAILURE when fetching user fails', () => {
fetchMock.getOnce('/api/users/1', 404);
const expectedActions = [
{ type: 'FETCH_USER_START' },
{
type: 'FETCH_USER_FAILURE',
payload: 'Network response was not ok',
error: true
}
];
const store = mockStore({ users: {} });
return store.dispatch(fetchUser(1)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
Action Creator Best Practices
Do's
- Use constants for action types to avoid typos
- Keep action creators pure functions when possible
- Validate inputs in action creators, not reducers
- Use descriptive names that indicate what happened
- Follow a consistent naming convention
- Use action creator factories for similar actions
Don'ts
- Don't perform side effects in synchronous action creators
- Don't mutate arguments passed to action creators
- Don't include non-serializable values in actions
- Don't make action creators overly complex
// Good practices
const goodActionCreator = (id, data) => {
// Validate inputs
if (!id) throw new Error('ID is required');
// Return plain object
return {
type: 'UPDATE_ITEM',
payload: {
id,
data: { ...data }, // Create new object
timestamp: Date.now()
}
// Could include meta for additional info
};
};
// Bad practices
const badActionCreator = (id, data) => {
// Don't perform side effects
localStorage.setItem('lastAction', 'UPDATE_ITEM'); // ❌
// Don't mutate arguments
data.modified = true; // ❌
// Don't include non-serializable values
return {
type: 'UPDATE_ITEM',
payload: {
id,
data,
element: document.getElementById('item') // ❌
}
};
};
Practice Exercise
Task: Create a Notification System
Implement action creators for a notification system with the following requirements:
- Show notifications (success, error, warning, info)
- Auto-dismiss after a specified duration
- Manual dismiss functionality
- Queue multiple notifications
// TODO: Define action types
const notificationTypes = {
// Your action types here
};
// TODO: Create action creators
const showNotification = (message, type = 'info', duration = 5000) => {
// Your implementation
};
const dismissNotification = (id) => {
// Your implementation
};
// TODO: Create an async action creator for auto-dismiss
const showTimedNotification = (message, type, duration) => {
// Your implementation
};
// Example usage:
// dispatch(showNotification('Success!', 'success'));
// dispatch(showTimedNotification('This will disappear', 'info', 3000));