Introduction to Redux Toolkit

What is Redux Toolkit?

Redux Toolkit (RTK) is the official, opinionated, batteries-included toolset for efficient Redux development. It's designed to solve common Redux pain points and establish best practices.

🧰 The Power Tools Analogy

Think of Redux Toolkit as power tools vs hand tools:

  • Traditional Redux: Like using hand tools - more control but more work
  • Redux Toolkit: Like using power tools - faster, safer, with built-in best practices
  • configureStore: A pre-configured workshop with all tools ready
  • createSlice: An all-in-one tool that combines multiple functions
  • createAsyncThunk: Automated machinery for complex operations

Just as power tools make construction faster and more consistent, Redux Toolkit makes Redux development more efficient and less error-prone.

Why Redux Toolkit?

graph TD A[Traditional Redux Problems] --> B[Boilerplate Code] A --> C[Complex Configuration] A --> D[Immutable Updates] A --> E[Side Effects] F[Redux Toolkit Solutions] --> G[Less Code] F --> H[Pre-configured] F --> I[Immer Integration] F --> J[Built-in Async] B --> G C --> H D --> I E --> J style A fill:#f96 style F fill:#9f9

Problems Redux Toolkit Solves

Before (Traditional Redux)


// Action Types
const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';

// Action Creators
const fetchUsersRequest = () => ({
  type: FETCH_USERS_REQUEST
});

const fetchUsersSuccess = (users) => ({
  type: FETCH_USERS_SUCCESS,
  payload: users
});

const fetchUsersFailure = (error) => ({
  type: FETCH_USERS_FAILURE,
  payload: error
});

// Async Action
const fetchUsers = () => {
  return async (dispatch) => {
    dispatch(fetchUsersRequest());
    try {
      const response = await api.getUsers();
      dispatch(fetchUsersSuccess(response.data));
    } catch (error) {
      dispatch(fetchUsersFailure(error.message));
    }
  };
};

// Reducer
const initialState = {
  users: [],
  loading: false,
  error: null
};

const usersReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_USERS_REQUEST:
      return {
        ...state,
        loading: true,
        error: null
      };
    case FETCH_USERS_SUCCESS:
      return {
        ...state,
        loading: false,
        users: action.payload
      };
    case FETCH_USERS_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    default:
      return state;
  }
};

// Store Configuration
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

const rootReducer = combineReducers({
  users: usersReducer
});

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);
                    

After (Redux Toolkit)


// All in one file with Redux Toolkit
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit';
import api from './api';

// Async Action
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async () => {
    const response = await api.getUsers();
    return response.data;
  }
);

// Slice (includes actions and reducer)
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    users: [],
    loading: false,
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  }
});

// Store Configuration
export const store = configureStore({
  reducer: {
    users: usersSlice.reducer
  }
});
                    

Core APIs of Redux Toolkit

1. configureStore


import { configureStore } from '@reduxjs/toolkit';
import usersReducer from './features/users/usersSlice';
import postsReducer from './features/posts/postsSlice';
import authReducer from './features/auth/authSlice';

// Basic configuration
const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    auth: authReducer
  }
});

// Advanced configuration
const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    auth: authReducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        // Ignore these action types
        ignoredActions: ['your/action/type'],
        // Ignore these field paths in all actions
        ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
        // Ignore these paths in the state
        ignoredPaths: ['items.dates']
      },
      immutableCheck: { warnAfter: 128 },
      thunk: {
        extraArgument: {
          api: apiService,
          env: process.env
        }
      }
    }).concat(customMiddleware),
  devTools: process.env.NODE_ENV !== 'production',
  preloadedState: {
    auth: { user: null, token: localStorage.getItem('token') }
  },
  enhancers: [customEnhancer]
});

// What configureStore does for you:
// 1. Combines reducers
// 2. Adds redux-thunk middleware
// 3. Adds development checks (immutability, serializability)
// 4. Sets up Redux DevTools
// 5. Applies middleware and enhancers
            

2. createSlice


import { createSlice } from '@reduxjs/toolkit';

// Simple slice
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => {
      state.value += 1; // Immer allows direct mutation
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

// Export actions
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Export reducer
export default counterSlice.reducer;

// More complex slice
const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    filter: 'all',
    loading: false,
    error: null
  },
  reducers: {
    addTodo: {
      reducer: (state, action) => {
        state.items.push(action.payload);
      },
      prepare: (text) => {
        return {
          payload: {
            id: Date.now(),
            text,
            completed: false,
            createdAt: new Date().toISOString()
          }
        };
      }
    },
    toggleTodo: (state, action) => {
      const todo = state.items.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo: (state, action) => {
      state.items = state.items.filter(todo => todo.id !== action.payload);
    },
    setFilter: (state, action) => {
      state.filter = action.payload;
    }
  },
  extraReducers: (builder) => {
    // Handle actions from other slices or async thunks
    builder
      .addCase('auth/logout', (state) => {
        // Clear todos on logout
        state.items = [];
      });
  }
});

// Selectors
export const selectAllTodos = state => state.todos.items;
export const selectTodoById = (state, todoId) => 
  state.todos.items.find(todo => todo.id === todoId);
export const selectFilteredTodos = state => {
  const { items, filter } = state.todos;
  switch (filter) {
    case 'active':
      return items.filter(todo => !todo.completed);
    case 'completed':
      return items.filter(todo => todo.completed);
    default:
      return items;
  }
};
            

3. createAsyncThunk


import { createAsyncThunk } from '@reduxjs/toolkit';
import { client } from '../../api/client';

// Basic async thunk
export const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId) => {
    const response = await client.get(`/users/${userId}`);
    return response.data;
  }
);

// Async thunk with condition
export const fetchUserIfNeeded = createAsyncThunk(
  'users/fetchIfNeeded',
  async (userId, { getState }) => {
    const { users } = getState();
    if (users.entities[userId]) {
      return users.entities[userId];
    }
    const response = await client.get(`/users/${userId}`);
    return response.data;
  },
  {
    condition: (userId, { getState }) => {
      const { users } = getState();
      return !users.loading && !users.entities[userId];
    }
  }
);

// Async thunk with error handling
export const loginUser = createAsyncThunk(
  'auth/login',
  async (credentials, { rejectWithValue }) => {
    try {
      const response = await client.post('/auth/login', credentials);
      // Save token to local storage
      localStorage.setItem('token', response.data.token);
      return response.data;
    } catch (err) {
      // Return custom error message
      return rejectWithValue(err.response.data.message);
    }
  }
);

// Async thunk with dispatch inside
export const checkoutCart = createAsyncThunk(
  'cart/checkout',
  async (paymentDetails, { dispatch, getState }) => {
    const { cart } = getState();
    
    // Create order
    const orderResponse = await client.post('/orders', {
      items: cart.items,
      total: cart.total
    });
    
    // Process payment
    const paymentResponse = await client.post('/payments', {
      orderId: orderResponse.data.id,
      ...paymentDetails
    });
    
    // Clear cart on success
    dispatch(clearCart());
    
    return {
      order: orderResponse.data,
      payment: paymentResponse.data
    };
  }
);

// Using in a slice
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    entities: {},
    loading: false,
    error: null,
    currentUser: null
  },
  reducers: {
    userUpdated: (state, action) => {
      const { id, changes } = action.payload;
      state.entities[id] = { ...state.entities[id], ...changes };
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.loading = false;
        state.entities[action.payload.id] = action.payload;
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.currentUser = action.payload.user;
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.error = action.payload; // Custom error message
      });
  }
});
            

Working with Immer

Redux Toolkit uses Immer internally, allowing you to write "mutative" code that produces immutable updates.


// Traditional Redux (without Immer)
const todosReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    case 'UPDATE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, ...action.payload.changes }
            : todo
        )
      };
    default:
      return state;
  }
};

// Redux Toolkit with Immer
const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    todos: []
  },
  reducers: {
    addTodo: (state, action) => {
      state.todos.push(action.payload); // "Mutate" directly
    },
    toggleTodo: (state, action) => {
      const todo = state.todos.find(todo => todo.id === action.payload.id);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    updateTodo: (state, action) => {
      const { id, changes } = action.payload;
      const todo = state.todos.find(todo => todo.id === id);
      if (todo) {
        Object.assign(todo, changes);
      }
    }
  }
});

// Advanced Immer patterns
const complexSlice = createSlice({
  name: 'complex',
  initialState: {
    users: {},
    posts: [],
    settings: {
      theme: 'light',
      notifications: true
    }
  },
  reducers: {
    // Deep updates are easy
    updateUserProfile: (state, action) => {
      const { userId, profileData } = action.payload;
      state.users[userId].profile = profileData;
    },
    // Array operations
    sortPosts: (state) => {
      state.posts.sort((a, b) => b.date - a.date);
    },
    // Complex manipulations
    reorganizeData: (state) => {
      // Remove unpublished posts
      state.posts = state.posts.filter(post => post.published);
      
      // Update user post counts
      state.posts.forEach(post => {
        state.users[post.authorId].postCount++;
      });
      
      // Toggle setting
      state.settings.notifications = !state.settings.notifications;
    }
  }
});

// Note: Don't return anything when using Immer
const incorrectReducer = (state, action) => {
  state.value = action.payload;
  return state; // ❌ Don't do this!
};

const correctReducer = (state, action) => {
  state.value = action.payload;
  // ✅ No return needed
};
            

Entity Adapters


import { createSlice, createEntityAdapter, createAsyncThunk } from '@reduxjs/toolkit';

// Create entity adapter
const usersAdapter = createEntityAdapter({
  // Optional: Specify ID field (default is 'id')
  selectId: (user) => user.userId,
  // Optional: Sort function
  sortComparer: (a, b) => a.name.localeCompare(b.name)
});

// Get initial state from adapter
const initialState = usersAdapter.getInitialState({
  loading: false,
  error: null,
  currentUser: null
});

// Async thunk
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
  const response = await api.getUsers();
  return response.data;
});

// Create slice with entity adapter
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    userAdded: usersAdapter.addOne,
    userUpdated: usersAdapter.updateOne,
    userRemoved: usersAdapter.removeOne,
    usersReceived: usersAdapter.setAll
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.fulfilled, (state, action) => {
        usersAdapter.setAll(state, action.payload);
        state.loading = false;
      });
  }
});

// Export entity selectors
export const {
  selectById: selectUserById,
  selectIds: selectUserIds,
  selectEntities: selectUserEntities,
  selectAll: selectAllUsers,
  selectTotal: selectTotalUsers
} = usersAdapter.getSelectors(state => state.users);

// Custom selectors
export const selectCurrentUser = state => 
  state.users.currentUser ? selectUserById(state, state.users.currentUser) : null;

// Using entity adapter methods
const postsAdapter = createEntityAdapter({
  sortComparer: (a, b) => b.date.localeCompare(a.date)
});

const postsSlice = createSlice({
  name: 'posts',
  initialState: postsAdapter.getInitialState(),
  reducers: {
    postAdded: postsAdapter.addOne,
    postUpdated: postsAdapter.updateOne,
    postsLoaded: postsAdapter.setAll,
    // Bulk operations
    postsAdded: postsAdapter.addMany,
    postsUpdated: postsAdapter.updateMany,
    postsRemoved: postsAdapter.removeMany,
    // Upsert operations (add if doesn't exist, update if does)
    postUpserted: postsAdapter.upsertOne,
    postsUpserted: postsAdapter.upsertMany
  }
});

// Usage in components
const PostsList = () => {
  const posts = useSelector(selectAllPosts);
  const postIds = useSelector(selectPostIds);
  const loading = useSelector(state => state.posts.loading);
  
  return (
    <div>
      {postIds.map(id => (
        <PostItem key={id} postId={id} />
      ))}
    </div>
  );
};

const PostItem = ({ postId }) => {
  const post = useSelector(state => selectPostById(state, postId));
  return <div>{post.title}</div>;
};
            

Real-World Example: Blog Application


// store.js
import { configureStore } from '@reduxjs/toolkit';
import postsReducer from './features/posts/postsSlice';
import usersReducer from './features/users/usersSlice';
import commentsReducer from './features/comments/commentsSlice';
import authReducer from './features/auth/authSlice';

export const store = configureStore({
  reducer: {
    posts: postsReducer,
    users: usersReducer,
    comments: commentsReducer,
    auth: authReducer
  }
});

// features/posts/postsSlice.js
import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { client } from '../../api/client';

const postsAdapter = createEntityAdapter({
  sortComparer: (a, b) => b.date.localeCompare(a.date)
});

const initialState = postsAdapter.getInitialState({
  status: 'idle',
  error: null
});

// Async thunks
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const response = await client.get('/api/posts');
  return response.data;
});

export const addNewPost = createAsyncThunk(
  'posts/addNewPost',
  async (initialPost) => {
    const response = await client.post('/api/posts', initialPost);
    return response.data;
  }
);

export const updatePost = createAsyncThunk(
  'posts/updatePost',
  async ({ id, changes }) => {
    const response = await client.patch(`/api/posts/${id}`, changes);
    return response.data;
  }
);

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postUpdated: postsAdapter.updateOne,
    reactionAdded(state, action) {
      const { postId, reaction } = action.payload;
      const existingPost = state.entities[postId];
      if (existingPost) {
        existingPost.reactions[reaction]++;
      }
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        postsAdapter.upsertMany(state, action.payload);
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      })
      .addCase(addNewPost.fulfilled, postsAdapter.addOne)
      .addCase(updatePost.fulfilled, (state, action) => {
        postsAdapter.updateOne(state, {
          id: action.payload.id,
          changes: action.payload
        });
      });
  }
});

export const { postUpdated, reactionAdded } = postsSlice.actions;

export default postsSlice.reducer;

// Selectors
export const {
  selectAll: selectAllPosts,
  selectById: selectPostById,
  selectIds: selectPostIds
} = postsAdapter.getSelectors(state => state.posts);

export const selectPostsByUser = createSelector(
  [selectAllPosts, (state, userId) => userId],
  (posts, userId) => posts.filter(post => post.user === userId)
);

// components/PostsList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectAllPosts, fetchPosts } from '../features/posts/postsSlice';
import { PostExcerpt } from './PostExcerpt';

export const PostsList = () => {
  const dispatch = useDispatch();
  const posts = useSelector(selectAllPosts);
  const postStatus = useSelector(state => state.posts.status);
  const error = useSelector(state => state.posts.error);

  useEffect(() => {
    if (postStatus === 'idle') {
      dispatch(fetchPosts());
    }
  }, [postStatus, dispatch]);

  let content;

  if (postStatus === 'loading') {
    content = <div>Loading...</div>;
  } else if (postStatus === 'succeeded') {
    content = posts.map(post => <PostExcerpt key={post.id} post={post} />);
  } else if (postStatus === 'failed') {
    content = <div>{error}</div>;
  }

  return (
    <section>
      <h2>Posts</h2>
      {content}
    </section>
  );
};

// components/AddPostForm.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addNewPost } from '../features/posts/postsSlice';
import { selectAllUsers } from '../features/users/usersSlice';

export const AddPostForm = () => {
  const dispatch = useDispatch();
  const users = useSelector(selectAllUsers);

  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [userId, setUserId] = useState('');
  const [addRequestStatus, setAddRequestStatus] = useState('idle');

  const canSave = [title, content, userId].every(Boolean) && addRequestStatus === 'idle';

  const onSavePostClicked = async () => {
    if (canSave) {
      try {
        setAddRequestStatus('pending');
        await dispatch(addNewPost({ title, content, user: userId })).unwrap();
        setTitle('');
        setContent('');
        setUserId('');
      } catch (err) {
        console.error('Failed to save the post: ', err);
      } finally {
        setAddRequestStatus('idle');
      }
    }
  };

  return (
    <section>
      <h2>Add a New Post</h2>
      <form>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          value={title}
          onChange={e => setTitle(e.target.value)}
        />

        <label htmlFor="postAuthor">Author:</label>
        <select id="postAuthor" value={userId} onChange={e => setUserId(e.target.value)}>
          <option value=""></option>
          {users.map(user => (
            <option key={user.id} value={user.id}>
              {user.name}
            </option>
          ))}
        </select>

        <label htmlFor="postContent">Content:</label>
        <textarea
          id="postContent"
          name="postContent"
          value={content}
          onChange={e => setContent(e.target.value)}
        />

        <button type="button" onClick={onSavePostClicked} disabled={!canSave}>
          Save Post
        </button>
      </form>
    </section>
  );
};
            

Migration from Traditional Redux


// Step 1: Install Redux Toolkit
// npm install @reduxjs/toolkit

// Step 2: Replace store configuration
// Before
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer
});

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

// After
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer
  }
});

// Step 3: Convert reducers to slices
// Before
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';

export const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { id: Date.now(), text, completed: false }
});

export const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: id
});

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

// After
import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: {
      reducer: (state, action) => {
        state.push(action.payload);
      },
      prepare: (text) => ({
        payload: {
          id: Date.now(),
          text,
          completed: false
        }
      })
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    }
  }
});

export const { addTodo, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;

// Step 4: Convert async actions to createAsyncThunk
// Before
const fetchTodosRequest = () => ({ type: 'FETCH_TODOS_REQUEST' });
const fetchTodosSuccess = (todos) => ({ type: 'FETCH_TODOS_SUCCESS', payload: todos });
const fetchTodosFailure = (error) => ({ type: 'FETCH_TODOS_FAILURE', payload: error });

export const fetchTodos = () => async (dispatch) => {
  dispatch(fetchTodosRequest());
  try {
    const response = await api.getTodos();
    dispatch(fetchTodosSuccess(response.data));
  } catch (error) {
    dispatch(fetchTodosFailure(error.message));
  }
};

// After
import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await api.getTodos();
    return response.data;
  }
);

// In the slice
extraReducers: (builder) => {
  builder
    .addCase(fetchTodos.pending, (state) => {
      state.status = 'loading';
    })
    .addCase(fetchTodos.fulfilled, (state, action) => {
      state.status = 'succeeded';
      state.todos = action.payload;
    })
    .addCase(fetchTodos.rejected, (state, action) => {
      state.status = 'failed';
      state.error = action.error.message;
    });
}
            

Best Practices

Do's

  • Use createSlice for all reducers
  • Keep slices focused on a single feature
  • Use entity adapters for normalized data
  • Write selectors in the slice file
  • Use createAsyncThunk for async operations
  • Take advantage of Immer for immutable updates

Don'ts

  • Don't mutate state outside of createSlice
  • Don't put business logic in components
  • Don't create circular dependencies between slices
  • Don't ignore TypeScript types (if using TS)
  • Don't put all state in Redux (local state is fine)

Folder Structure


src/
  app/
    store.js
  features/
    users/
      usersSlice.js
      Users.js
      UserDetail.js
    posts/
      postsSlice.js
      PostsList.js
      AddPostForm.js
    comments/
      commentsSlice.js
      CommentsList.js
  components/
    Header.js
    Footer.js
  api/
    client.js
  App.js
  index.js
                

Practice Exercise

Task: Create a Todo App with Redux Toolkit

Build a todo application using Redux Toolkit that includes:

  • Todo CRUD operations
  • Filter todos (all, active, completed)
  • Async operations (fetch, save)
  • Loading states and error handling
  • Using entity adapter for todos

// TODO: Create the todo slice
import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';

// TODO: Create entity adapter
const todosAdapter = // ...

// TODO: Create async thunks
export const fetchTodos = createAsyncThunk(/* ... */);
export const saveTodo = createAsyncThunk(/* ... */);

// TODO: Create the slice
const todosSlice = createSlice({
  name: 'todos',
  initialState: // ...,
  reducers: {
    // TODO: Add reducers
  },
  extraReducers: (builder) => {
    // TODO: Handle async actions
  }
});

// TODO: Export actions and selectors

// TODO: Create the store
import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {
    // TODO: Add reducers
  }
});

// TODO: Create components using the Redux store
                

Additional Resources