Understanding Redux Actions

The Messengers of State Change in Redux

Introduction to Redux Actions

In the Redux ecosystem, actions are the messengers that tell your application what changes need to happen. Think of actions as delivery people bringing messages to your store. They don't actually change anything themselves - they just carry the information about what needs to change.

Actions in Redux are plain JavaScript objects (often called Plain Old JavaScript Objects or POJOs) that must have a type property. This property describes the kind of change that should happen. Actions may also contain additional data called the payload.

Action Structure

{
  type: 'ADD_FRUIT',  // Required - describes what kind of change
  fruit: 'apple'      // Optional - additional data (payload)
}

Action Creators: The Factory Pattern

Imagine a factory that builds cars. You don't assemble each car by hand every time - you create a factory with a blueprint. Similarly, action creators are functions that create action objects. They make your code more maintainable and reusable.

Basic Action Creator

const addFruit = (fruit) => {
  return {
    type: 'ADD_FRUIT',
    fruit
  };
};

In this example, addFruit is a function that takes a fruit parameter and returns an action object. This pattern follows the "factory pattern" in software design, where a function creates objects according to a specific template.

Shortened Syntax with Implicit Return

const addFruit = (fruit) => ({
  type: 'ADD_FRUIT',
  fruit
});

Debugging Note: While the implicit return syntax is cleaner, it makes debugging more difficult since you can't add console.log statements or use the debugger statement inside the function body without changing the syntax back to use explicit return.

Dispatching Actions

Creating an action is like writing a letter - it doesn't do anything until you send it. In Redux, you "send" actions using the store.dispatch() method. This is the only way to trigger a state change in Redux.

Dispatching an Action

// Create and dispatch in one step
store.dispatch(addFruit('apple'));

// Or create the action first, then dispatch
const appleAction = addFruit('apple');
store.dispatch(appleAction);

When you dispatch an action, Redux passes it through all your reducers, which then determine if and how the state should change based on the action type.

Real-world example: This is similar to how you might interact with a grocery inventory system. If a new shipment of apples arrives, a store manager doesn't manually update each department's records. Instead, they submit a form (the action) to the inventory system, which then updates all the relevant databases and notifies the appropriate departments.

Action Types and Constants

Using string literals directly for action types can lead to errors that are hard to debug. Imagine if you had to catch a spelling error between 'ADD_FRUIT' and 'ADD_FRIUT' across a large application!

To prevent typos and make debugging easier, we define action types as constants:

Using Constants for Action Types

// Define the constant
const ADD_FRUIT = 'ADD_FRUIT';

// Use it in your action creator
const addFruit = (fruit) => ({
  type: ADD_FRUIT,
  fruit
});

// And in your reducer
function fruitReducer(state = [], action) {
  switch (action.type) {
    case ADD_FRUIT: // Same constant used here
      return [...state, action.fruit];
    default:
      return state;
  }
}

This approach ensures that if you mistype the constant name, JavaScript will throw an error (undefined variable) rather than silently creating a bug.

Ducks Pattern for Action Types

For larger applications, action type naming can get confusing if multiple parts of your app use similar action names. The "Ducks" pattern proposes a naming convention to avoid conflicts:

Ducks Naming Convention

// Format: 'appName/feature/ACTION_TYPE'
const ADD_FRUIT = 'groceryApp/fruits/ADD_FRUIT';
const REMOVE_FRUIT = 'groceryApp/fruits/REMOVE_FRUIT';

// Used in action creators
const addFruit = (fruit) => ({
  type: ADD_FRUIT,
  fruit
});

This naming convention works like a namespacing system, similar to how domain names on the internet ensure uniqueness (e.g., company.com vs. person.com).

Folder structure example: In a typical React-Redux application following the Ducks pattern, you might have a folder structure like:

src/
  features/
    fruits/
      fruitsActions.js     // Action creators and constants
      fruitsReducer.js     // Reducer function
      fruitsSelectors.js   // Functions to extract data from state
    vegetables/
      vegetablesActions.js
      vegetablesReducer.js
      vegetablesSelectors.js

Actions vs. Reducers: Understanding the Workflow

A common confusion for beginners is understanding the relationship between actions and reducers. Let's clarify with an analogy:

Restaurant Analogy

Actions are like customer orders: "I'd like a fruit salad with apples and bananas."

Reducers are like chefs: They receive the order (action) and know how to prepare the dish (update the state).

Store is like the restaurant itself: It takes the orders and ensures they get to the chef.

It's crucial to remember that actions only trigger state changes; they don't implement them. Actions define what should happen, while reducers define how it happens.

Complete Redux Flow Example

// 1. Define action types
const ADD_FRUIT = 'groceryApp/fruits/ADD_FRUIT';

// 2. Create action creators
const addFruit = (fruit) => ({
  type: ADD_FRUIT,
  fruit
});

// 3. Define reducer
function fruitsReducer(state = [], action) {
  switch(action.type) {
    case ADD_FRUIT:
      return [...state, action.fruit];
    default:
      return state;
  }
}

// 4. Create store (simplified)
const store = createStore(fruitsReducer);

// 5. Dispatch action
store.dispatch(addFruit('apple'));
// Now state is ['apple']

store.dispatch(addFruit('banana'));
// Now state is ['apple', 'banana']

Practical Exercise: Building a Todo List

Let's apply what we've learned by creating action creators for a simple todo application.

Todo List Action Creators

Step 1: Create a file named todoActions.js in your project's src/features/todos/ folder.

Step 2: Define action type constants:

// src/features/todos/todoActions.js
export const ADD_TODO = 'todoApp/todos/ADD_TODO';
export const TOGGLE_TODO = 'todoApp/todos/TOGGLE_TODO';
export const SET_VISIBILITY_FILTER = 'todoApp/todos/SET_VISIBILITY_FILTER';

Step 3: Create action creators:

// src/features/todos/todoActions.js
// Action to add a new todo
export const addTodo = (text) => ({
  type: ADD_TODO,
  payload: {
    id: Date.now(), // Simple way to generate unique IDs
    text,
    completed: false
  }
});

// Action to toggle a todo's completed status
export const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: { id }
});

// Action to change the visibility filter
export const setVisibilityFilter = (filter) => ({
  type: SET_VISIBILITY_FILTER,
  payload: { filter }
});

Step 4: Create a reducer in todoReducer.js:

// src/features/todos/todoReducer.js
import { ADD_TODO, TOGGLE_TODO } from './todoActions';

const initialState = [];

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
      
    case TOGGLE_TODO:
      return state.map(todo => 
        todo.id === action.payload.id 
          ? { ...todo, completed: !todo.completed } 
          : todo
      );
      
    default:
      return state;
  }
}

Step 5: Use in a component:

// src/components/AddTodo.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../features/todos/todoActions';

function AddTodo() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch(addTodo(text));
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

Advanced Action Concepts

Async Actions and Middleware

Sometimes actions need to do asynchronous work (like API calls) before they can dispatch the final action. This is where middleware like Redux-Thunk comes in. With Redux-Thunk, action creators can return functions instead of objects, allowing for async logic.

Thunk Action Creator Example

// Action types
const FETCH_FRUITS_REQUEST = 'groceryApp/fruits/FETCH_FRUITS_REQUEST';
const FETCH_FRUITS_SUCCESS = 'groceryApp/fruits/FETCH_FRUITS_SUCCESS';
const FETCH_FRUITS_FAILURE = 'groceryApp/fruits/FETCH_FRUITS_FAILURE';

// Regular action creators
const fetchFruitsRequest = () => ({ type: FETCH_FRUITS_REQUEST });
const fetchFruitsSuccess = (fruits) => ({ 
  type: FETCH_FRUITS_SUCCESS, 
  payload: fruits 
});
const fetchFruitsFailure = (error) => ({ 
  type: FETCH_FRUITS_FAILURE, 
  error 
});

// Thunk action creator
const fetchFruits = () => {
  return async (dispatch) => {
    dispatch(fetchFruitsRequest());
    try {
      const response = await fetch('/api/fruits');
      const data = await response.json();
      dispatch(fetchFruitsSuccess(data));
    } catch (error) {
      dispatch(fetchFruitsFailure(error.toString()));
    }
  };
};

This pattern is like a delivery service that needs to go pick up a package before it can deliver it to your store.

Action Payloads Best Practices

While Redux doesn't enforce any structure for the action payload, following consistent patterns makes your code more maintainable:

Flux Standard Action (FSA) Pattern

// Good - follows FSA pattern
const addFruit = (fruit) => ({
  type: ADD_FRUIT,
  payload: fruit,
  // Optional properties
  meta: { timestamp: Date.now() },
  error: false
});

// If there's an error
const fruitError = (errorMessage) => ({
  type: FRUIT_ERROR,
  payload: new Error(errorMessage),
  error: true
});

Following the FSA pattern is like using standardized shipping containers - it makes your actions consistent and predictable.

Real-world Applications

Actions are used in various ways in production applications:

E-commerce

In an e-commerce site, actions might include:

Authentication

For user authentication, you might have:

Social Media

A social media app might include:

Common Mistakes and Debugging

Action Type Typos

One of the most common errors in Redux is mistyping action types. Using constants helps prevent this, but here's what happens when there's a mismatch:

// Action creator
const addFruit = (fruit) => ({
  type: 'ADD_FRIUT', // Typo here!
  fruit
});

// Reducer
function fruitsReducer(state = [], action) {
  switch(action.type) {
    case 'ADD_FRUIT': // Doesn't match the action type
      return [...state, action.fruit];
    default:
      return state;
  }
}

// Result: Action gets dispatched but state doesn't change
// because the reducer doesn't recognize the action type

Mutating State in Actions

Another common mistake is trying to modify state directly in the action creator:

Incorrect Approach

// WRONG - Don't do this!
const addFruit = (fruit) => {
  state.fruits.push(fruit); // Trying to modify state directly
  return {
    type: ADD_FRUIT
  };
};

Correct Approach

// RIGHT - Actions just carry information
const addFruit = (fruit) => ({
  type: ADD_FRUIT,
  fruit
});

// The reducer handles the state update
function fruitsReducer(state = [], action) {
  switch(action.type) {
    case ADD_FRUIT:
      return [...state, action.fruit]; // Creates new state
    default:
      return state;
  }
}

Debugging Actions

To debug actions, you can use Redux DevTools or add logging:

// Simple action logging middleware
const loggerMiddleware = store => next => action => {
  console.log('Dispatching action:', action);
  const result = next(action);
  console.log('New state:', store.getState());
  return result;
};

Summary and Further Learning

Key Takeaways

  • Actions are plain JavaScript objects with a type property that describe what changes should happen.
  • Action creators are functions that create action objects, making your code more maintainable and reusable.
  • Use constants for action types to prevent typos and make debugging easier.
  • The Ducks pattern provides a naming convention for action types in larger applications.
  • Actions trigger state changes but don't implement them; reducers handle the actual state updates.

Topics to Explore Next

  • Redux Middleware for async actions (Redux-Thunk, Redux-Saga)
  • Normalization of Redux Store Data
  • Selectors and Reselect library
  • Redux Toolkit for simplified Redux logic
  • React-Redux Hooks API

Challenge Exercise

Now that you understand action creators, try creating a complete set of action creators and reducers for a movie watchlist application. Your application should support:

Create the necessary action type constants, action creators, and reducers. Bonus points for adding async action creators to fetch movie data from an API!