L3: Create Listings Slice
Build the listings slice with initial state and reducers
Now that Redux is connected, let's create our first slice! A slice contains all the Redux logic for a specific feature.
What is a Slice?
A slice is a collection of Redux reducer logic and actions for a single feature:
const listingsSlice = createSlice({
name: 'listings', // Feature name
initialState: { items: [], ...}, // Starting state
reducers: { // State update functions
toggleFavorite: (state, action) => {
// Update state...
}
}
});One slice = one feature's complete Redux logic!
Why "slice"?
Your Redux state is like a pie 🥧. Each feature gets its own slice:
{
listings: { ... }, // ← Listings slice
user: { ... }, // ← User slice
cart: { ... } // ← Cart slice
}Benefits of Slices
Redux Toolkit's createSlice() provides huge benefits:
Traditional Redux: ~50 lines per feature
// Action types
const TOGGLE_FAVORITE = 'listings/toggleFavorite';
const SET_LISTINGS = 'listings/setListings';
// Action creators
const toggleFavorite = (id) => ({
type: TOGGLE_FAVORITE,
payload: id
});
const setListings = (listings) => ({
type: SET_LISTINGS,
payload: listings
});
// Reducer
const listingsReducer = (state = initialState, action) => {
switch (action.type) {
case TOGGLE_FAVORITE:
return {
...state,
favorites: state.favorites.includes(action.payload)
? state.favorites.filter(id => id !== action.payload)
: [...state.favorites, action.payload]
};
case SET_LISTINGS:
return {
...state,
items: action.payload
};
default:
return state;
}
};Redux Toolkit Slice: ~15 lines
const listingsSlice = createSlice({
name: 'listings',
initialState: { items: [], favorites: [] },
reducers: {
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(fav => fav !== id);
} else {
state.favorites.push(id);
}
},
setListings: (state, action) => {
state.items = action.payload;
}
}
});70% less code! 🎉
The Problem: Redux requires immutability
// ❌ BAD: Mutates state directly
state.favorites.push(id);
// ✅ GOOD: Creates new array
state.favorites = [...state.favorites, id];The Solution: Redux Toolkit uses Immer
// Looks like mutation, but actually creates new state!
state.favorites.push(id); // ✅ Safe with Immer!Immer tracks changes and creates new immutable state automatically.
You can write "mutating" code safely:
reducers: {
addItem: (state, action) => {
state.items.push(action.payload); // ✅ Works!
},
removeItem: (state, action) => {
state.items.splice(index, 1); // ✅ Works!
},
updateItem: (state, action) => {
state.items[0].name = 'New'; // ✅ Works!
}
}The Problem: Manually creating actions
// Action types
const ADD_TODO = 'todos/add';
// Action creators
const addTodo = (text) => ({
type: ADD_TODO,
payload: text
});
// Usage
dispatch(addTodo('Buy milk'));The Solution: Actions created automatically
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload);
}
}
});
// Auto-generated action creator!
const { addTodo } = todosSlice.actions;
// Usage (same as before)
dispatch(addTodo('Buy milk'));Action creators are automatically generated from reducer names!
Planning Our Listings Slice
Let's plan what our listings slice needs:
State Structure:
{
items: [], // All listings from API
favorites: [], // Array of favorited listing IDs
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null // Error message if fetch fails
}Reducers (State Updates):
toggleFavorite- Add/remove listing from favorites- Later: Handle async fetching (next lessons)
Step 1: Create Slice File
Create a new directory and file:
mkdir -p src/state/slices
touch src/state/slices/listingsSlice.jsStep 2: Import createSlice
Open src/state/slices/listingsSlice.js and import:
import { createSlice } from '@reduxjs/toolkit';createSlice is the core Redux Toolkit function for creating slices.
Step 3: Define Initial State
Add the initial state:
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null,
};What is each field for?
Step 4: Create the Slice
Now create the slice with toggleFavorite reducer:
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null,
};
const listingsSlice = createSlice({
name: 'listings',
initialState,
reducers: {
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
// Remove from favorites
state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
} else {
// Add to favorites
state.favorites.push(id);
}
},
},
});What's happening here?
createSlice Configuration
const listingsSlice = createSlice({
name: 'listings', // Slice name (appears in DevTools)
initialState, // Starting state
reducers: { ... } // State update functions
});The name is used for:
- Action types:
listings/toggleFavorite - Redux DevTools display
- Error messages
toggleFavorite Reducer
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
} else {
state.favorites.push(id);
}
}Parameters:
state- Current state (can be "mutated" safely thanks to Immer)action- Action object:{ type: 'listings/toggleFavorite', payload: 123 }
Logic:
- Extract listing ID from
action.payload - Check if already favorited
- If yes → remove from array
- If no → add to array
Why can we mutate state?
Immer converts this:
state.favorites.push(id);Into this:
return {
...state,
favorites: [...state.favorites, id]
};Automatically! You write simple code, Redux Toolkit handles immutability.
Step 5: Export Actions and Reducer
Add exports at the end of the file:
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null,
};
const listingsSlice = createSlice({
name: 'listings',
initialState,
reducers: {
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
} else {
state.favorites.push(id);
}
},
},
});
export const { toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;What are we exporting?
Complete Code
Here's the complete listingsSlice.js:
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null,
};
const listingsSlice = createSlice({
name: 'listings',
initialState,
reducers: {
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
} else {
state.favorites.push(id);
}
},
},
});
export const { toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;Only 28 lines for a complete Redux feature! 🎉
Understanding the Flow
When you dispatch toggleFavorite(123):
Action is Created
dispatch(toggleFavorite(123));
// Creates: { type: 'listings/toggleFavorite', payload: 123 }Redux Calls Reducer
reducer(state, { type: 'listings/toggleFavorite', payload: 123 })State is Updated
// Before
{ favorites: [1, 5, 9] }
// After (123 added)
{ favorites: [1, 5, 9, 123] }Components Re-render
All components using useSelector(state => state.listings.favorites) automatically re-render with the new state!
What's Next?
Excellent! We've created the listings slice. In the next lesson, we'll:
- Add the slice to the store - Register our reducer
- See it in Redux DevTools - Verify it works
- Test the state structure - Confirm setup is correct
✅ Lesson Complete! You've created your first Redux slice with state and a reducer!
Key Takeaways
- ✅ A slice contains all Redux logic for one feature
- ✅
createSlice()generates actions and reducers automatically - ✅ Write "mutating" code safely with Immer
- ✅ Export actions for components to dispatch
- ✅ Export reducer for store configuration
- ✅ 70% less boilerplate compared to traditional Redux!