📨 Actions & Action Creators
If the store is a bank vault, actions are the deposit slips — the only messages allowed through the counter. An action is a plain object that describes what happened, and an action creator is a small function that builds one for you. Getting these right keeps your app's history readable and your reducers simple.
Week 6 · Day 1 (Monday: Redux Fundamentals) · Lecture 2
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Describe the anatomy of an action object and the required
typefield - Apply the Flux Standard Action (FSA) convention for consistent action shapes
- Name action types clearly using the
domain/eventNamepattern - Write reusable action creators, including ones that validate or prepare their payload
- Explain how Redux Toolkit's
createSlicegenerates action creators automatically - Write a simple unit test for an action creator
Estimated Time: 65 minutes
Practice: Build a set of action creators for a notification system and test one.
In This Lesson
What Is an Action?
An action is a plain JavaScript object that describes something that happened in your app. It is the only way to send data into the Redux store. Every action must have a type property — a string that tells reducers which change is being requested — and it usually carries some data in a payload.
📮 The Mail System Analogy
Think of actions as pieces of mail:
- Action — a letter describing what should happen
- type — the address that routes it to the right department
- payload — the contents of the envelope (the data)
- Action creator — the form or clerk that writes a properly-formatted letter
- dispatch — dropping the letter into the mailbox
Mail only gets delivered if it follows the postal format. Likewise, actions only get processed if they follow Redux's conventions — a fact that keeps your entire codebase speaking the same language.
Actions are descriptive, not imperative. A good action says "an item was added to the cart" — it reports an event. It does not say "go mutate the cart array." How the state changes is the reducer's job; the action only carries the news.
Anatomy of an Action
A minimal action is just a type. Most carry a payload. Two optional fields — meta and error — round out the standard shape.
type. payload carries data; meta and error are optional extras from the FSA convention.The Flux Standard Action (FSA) convention
Redux itself only requires a type. But teams adopt the Flux Standard Action shape so every action looks the same: a type, an optional payload, an optional meta, and an optional boolean error. Consistency means any developer can read any action at a glance.
// A basic action — just a type:
{ type: 'counter/incremented' }
// With a payload carrying data:
{
type: 'todos/added',
payload: { id: 1, text: 'Learn Redux actions', completed: false }
}
// An error action (note error: true):
{
type: 'user/fetchFailed',
payload: new Error('User not found'),
error: true
}
// With meta — extra info that isn't part of the data itself:
{
type: 'analytics/eventTracked',
payload: { eventName: 'button_click' },
meta: { timestamp: Date.now() }
}
⚠️ Keep actions serializable
Put only plain, serializable data in actions — strings, numbers, booleans, plain objects, arrays. Avoid functions, Promises, DOM nodes, or class instances. Serializable actions are what make DevTools logging, time-travel, and persistence possible.
Naming Action Types
Action types are strings, and how you name them shapes how readable your app's history is. The modern convention, recommended by the Redux Style Guide, is domain/eventName — the feature area, a slash, then a past-tense description of what happened.
// ✅ Good — domain/eventName, past tense, describes an event:
'user/loggedIn'
'cart/itemAdded'
'posts/fetchSucceeded'
'posts/fetchFailed'
// ❌ Vague — you can't tell what changed or where:
'UPDATE'
'CHANGE_STATE'
'SET_DATA'
You'll also see older code use SCREAMING_SNAKE_CASE constants like ADD_TODO. That style still works, and extracting types into named constants prevents typos. But for new code, prefer the domain/eventName string.
// Extracting types as constants avoids silent typo bugs:
const TODO_ADDED = 'todos/added';
const TODO_TOGGLED = 'todos/toggled';
const TODO_REMOVED = 'todos/removed';
// Grouping a feature's types in one object is common:
export const todoTypes = {
ADDED: 'todos/added',
TOGGLED: 'todos/toggled',
REMOVED: 'todos/removed'
};
💡 Describe events, not commands
Prefer cart/itemAdded (an event that occurred) over cart/addItem (a command). Event names read like a log of history — which is exactly how Redux DevTools presents them — and let multiple reducers respond to the same event independently.
Action Creators
Writing action objects by hand everywhere is repetitive and typo-prone. An action creator is a function that builds and returns an action object. Define it once, call it anywhere — and if the action's shape ever changes, you update one function instead of dozens of call sites.
// A simple action creator:
const todoAdded = (text) => ({
type: 'todos/added',
payload: { id: Date.now(), text, completed: false }
});
// Shorthand for a single-value payload:
const todoToggled = (id) => ({
type: 'todos/toggled',
payload: id
});
// Dispatching becomes clean and consistent:
dispatch(todoAdded('Learn action creators'));
dispatch(todoToggled(123));
Compare the two styles. Without a creator, every component that adds a todo repeats the full object literal — and each is a chance to misspell 'todos/added' or forget completed. With a creator, the shape is defined once and guaranteed correct at every call site.
Useful Creator Patterns
Validating input
Action creators are a good place to guard against bad input — before it ever reaches a reducer. Reducers should stay pure and dumb; creators can be a little smarter.
const todoUpdated = (id, updates) => {
if (!id) {
throw new Error('todoUpdated requires an id');
}
return {
type: 'todos/updated',
payload: { id, updates: { ...updates } } // copy, don't mutate the arg
};
};
Preparing a payload
Creators can compute derived fields — an id, a timestamp — so callers don't have to.
import { v4 as uuidv4 } from 'uuid';
const userAdded = (name, email) => ({
type: 'users/added',
payload: {
id: uuidv4(),
name,
email,
createdAt: new Date().toISOString()
}
});
⚠️ Careful with impurity in creators
Generating an id or timestamp in a creator is fine — reducers must stay pure, and this keeps that impurity out of them. But it does make the creator non-deterministic, which matters for testing (use expect.objectContaining) and is exactly the concern RTK's prepare callback formalizes.
An action creator factory
When several features need the same set of actions, a factory that returns creators avoids copy-paste.
// Build a matching set of CRUD creators for any resource:
const makeCrudCreators = (resource) => ({
created: (data) => ({ type: `${resource}/created`, payload: data }),
updated: (id, data) => ({ type: `${resource}/updated`, payload: { id, data } }),
removed: (id) => ({ type: `${resource}/removed`, payload: id })
});
const userActions = makeCrudCreators('users');
const postActions = makeCrudCreators('posts');
dispatch(userActions.created({ name: 'Ada' }));
dispatch(postActions.removed(42));
Bound action creators
Sometimes you want a function that dispatches automatically, so callers don't repeat dispatch(...). Redux ships bindActionCreators for exactly this — though in modern React you'll usually just call dispatch from the useDispatch hook.
import { bindActionCreators } from 'redux';
const creators = { todoAdded, todoToggled };
const bound = bindActionCreators(creators, store.dispatch);
bound.todoAdded('Buy milk'); // dispatches automatically
bound.todoToggled(7);
Action Creators the RTK Way
Here's the good news: with Redux Toolkit, you rarely write action creators or type constants by hand. When you define a slice, RTK generates a matching action creator for every reducer — correctly named, correctly shaped, ready to dispatch.
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
// Each key becomes an action creator AND its type:
added(state, action) { state.push(action.payload); },
toggled(state, action) {
const todo = state.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
}
}
});
// Auto-generated creators. Their .type is 'todos/added', 'todos/toggled':
export const { added, toggled } = todosSlice.actions;
dispatch(added({ id: 1, text: 'Ship it', completed: false }));
console.log(added.type); // 'todos/added' ← the domain/eventName format, for free
Need custom payload preparation (like generating an id)? RTK's prepare callback keeps that logic tidy and out of your reducers:
import { createSlice, nanoid } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
added: {
reducer(state, action) { state.push(action.payload); },
prepare(text) {
// Runs before the reducer; shapes the payload:
return { payload: { id: nanoid(), text, completed: false } };
}
}
}
});
dispatch(todosSlice.actions.added('Learn RTK'));
💡 Same concepts, less typing
Notice that nothing changed conceptually: an action is still { type, payload }, still described in the domain/eventName style, still the only way into the store. RTK just generates the boilerplate you'd otherwise write by hand.
Testing Action Creators
Because a plain action creator is a pure function that returns an object, it's one of the easiest things in your app to test: call it, and assert on what comes back.
import { todoAdded, todoToggled } from './todoActions';
describe('todo action creators', () => {
it('todoAdded returns a well-formed action', () => {
const action = todoAdded('Finish the docs');
// The id uses Date.now(), so match loosely:
expect(action).toEqual({
type: 'todos/added',
payload: expect.objectContaining({
text: 'Finish the docs',
completed: false
})
});
});
it('todoToggled carries the id as its payload', () => {
expect(todoToggled(123)).toEqual({
type: 'todos/toggled',
payload: 123
});
});
});
Testing an action creator that generates non-deterministic values (like a timestamp) is exactly why expect.objectContaining exists — assert on the fields you control, and ignore the ones you don't.
Practice & Quiz
🏋️ Exercise 1: Notification action creators
Goal: Write two action creators for a notification system. notificationShown(message, level) should produce an action carrying a unique id, the message, and a level that defaults to 'info'. notificationDismissed(id) should carry the id as its payload.
let nextId = 1;
const notificationShown = (message, level = 'info') => {
// TODO: return an action of type 'notifications/shown'
};
const notificationDismissed = (id) => {
// TODO: return an action of type 'notifications/dismissed'
};
💡 Hint
Each creator returns an object literal with a type and a payload. For notificationShown, build the payload as { id: nextId++, message, level }. Give level a default parameter of 'info'.
✅ Solution
const notificationShown = (message, level = 'info') => ({
type: 'notifications/shown',
payload: { id: nextId++, message, level }
});
const notificationDismissed = (id) => ({
type: 'notifications/dismissed',
payload: id
});
🏋️ Exercise 2: Validate before creating
Goal: Write quantityUpdated(productId, quantity) that throws if quantity is negative, and otherwise returns an action of type cart/quantityUpdated with a { productId, quantity } payload.
✅ Solution
const quantityUpdated = (productId, quantity) => {
if (quantity < 0) {
throw new Error('quantity cannot be negative');
}
return {
type: 'cart/quantityUpdated',
payload: { productId, quantity }
};
};
Validating in the creator keeps the reducer pure and simple — it can trust the action it receives.
🎯 Quick Quiz
Question 1: Which property is required on every Redux action?
Question 2: Which action type follows the recommended modern convention?
Question 3: With Redux Toolkit's createSlice, where do your action creators come from?
Best Practices & Pitfalls
✅ Do
- Give every action a clear
typeindomain/eventNameform - Name actions after events that happened, not commands to execute
- Use action creators to keep action shapes consistent and typo-free
- Validate or prepare payloads in the creator, keeping reducers pure
- Let Redux Toolkit generate creators and types when you can
❌ Don't
- Put non-serializable values (functions, Promises, DOM nodes) in an action
- Perform side effects (network calls,
localStoragewrites) inside a synchronous creator - Mutate arguments passed into a creator — copy them instead
- Cram unrelated concerns into one giant action
✅ Async lives in thunks, not plain creators
A plain action creator returns an object and stays synchronous. When you need to talk to an API, that logic belongs in a thunk (with redux-thunk, included in RTK) or in RTK's createAsyncThunk — which dispatch pending/fulfilled/rejected actions around the request. You'll dive into async flows later in the module.
Summary
🎉 Key Takeaways
- An action is a plain, serializable object describing what happened; only
typeis required - The FSA convention —
type,payload, optionalmeta/error— keeps actions uniform - Name types
domain/eventNamein past tense so they read like history - Action creators are functions that build actions — reusable, testable, typo-proof
- Redux Toolkit generates creators and types for you via
createSlice
📚 Additional Resources
- Redux Fundamentals — Actions
- Redux Style Guide — Action Type Naming
- Redux — Action Creators & Reducing Boilerplate
- Redux Toolkit — createSlice API
🚀 What's Next?
You can now describe what happened with clean, well-named actions. Next you'll learn who listens to them: Reducers — the pure functions that read an action and compute the next state, immutably.
🎉 Nicely done!
Clear actions are the vocabulary of a Redux app. Everything the store does is a response to one of them.