Code To Learn logo

Code To Learn

M6: State ManagementRedux Toolkit Path

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.js

Step 2: Import createSlice

Open src/state/slices/listingsSlice.js and import:

src/state/slices/listingsSlice.js
import { createSlice } from '@reduxjs/toolkit';

createSlice is the core Redux Toolkit function for creating slices.

Step 3: Define Initial State

Add the initial state:

src/state/slices/listingsSlice.js
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:

src/state/slices/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)) {
        // 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:

  1. Extract listing ID from action.payload
  2. Check if already favorited
  3. If yes → remove from array
  4. 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:

src/state/slices/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;

What are we exporting?

Complete Code

Here's the complete listingsSlice.js:

src/state/slices/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:

  1. Add the slice to the store - Register our reducer
  2. See it in Redux DevTools - Verify it works
  3. 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!