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:
ADD_TO_CART- When a user adds a product to their cartREMOVE_FROM_CART- When a user removes an itemUPDATE_QUANTITY- When a user changes item quantityAPPLY_COUPON- When a user applies a discount codeCHECKOUT_START- When a user begins the checkout process
Authentication
For user authentication, you might have:
LOGIN_REQUEST- When login form is submittedLOGIN_SUCCESS- When login succeedsLOGIN_FAILURE- When login failsLOGOUT- When user logs out
Social Media
A social media app might include:
POST_CREATED- When a user creates a new postCOMMENT_ADDED- When a user commentsLIKE_TOGGLED- When a user likes/unlikes contentFEED_REFRESHED- When new content is loaded
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
typeproperty 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:
- Adding movies to a watchlist
- Removing movies from the watchlist
- Marking movies as watched
- Rating movies (1-5 stars)
- Filtering the list by watched/unwatched status
- Sorting the list by rating, title, or date added
Create the necessary action type constants, action creators, and reducers. Bonus points for adding async action creators to fetch movie data from an API!