Redux Actions and Action Creators

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

Additional Resources