Setting up Redux Toolkit for the blog application involves three steps: create slices for each domain (posts, auth), configure the store from those slices, and wrap the React app with the Redux Provider. Components then connect to the store using useSelector to read state and useDispatch to send actions. This lesson builds the complete store setup and a fully working posts slice that components can interact with.
Store Setup
// src/store/index.js — the Redux store
import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "./postsSlice";
import authReducer from "./authSlice";
import uiReducer from "./uiSlice";
export const store = configureStore({
reducer: {
posts: postsReducer,
auth: authReducer,
ui: uiReducer,
},
// configureStore automatically:
// - Adds redux-thunk middleware
// - Enables Redux DevTools Extension
// - Checks for accidental state mutations in development
});
// TypeScript users: export RootState and AppDispatch types here
// export type RootState = ReturnType<typeof store.getState>;
// export type AppDispatch = typeof store.dispatch;
// src/main.jsx — wrap the app with Provider
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { store } from "@/store";
import App from "./App.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</StrictMode>
);
Note:
configureStore automatically adds redux-thunk middleware, enables the Redux DevTools Extension integration, and in development mode adds serializability-check and immutability-check middleware that warn you if you accidentally put non-serializable values (like class instances or functions) in the store, or if you accidentally mutate state outside of Immer. These checks help catch subtle bugs early and are automatically disabled in production builds for performance.Tip: Name your selectors explicitly and colocate them with the slice:
export const selectAllPosts = s => s.posts.items, export const selectPostById = (id) => s => s.posts.items.find(p => p.id === id). Exporting named selectors from the slice file means you can change the state shape in one place without updating every component that reads it. Components import and use the selector: const posts = useSelector(selectAllPosts).Warning: Every component that calls
useSelector re-renders when its selected value changes. If you select the entire posts array (s => s.posts.items), the component re-renders whenever any post is added, removed, or updated — even if the component only renders one post. For performance-sensitive lists, use useSelector(s => s.posts.items[index]) or RTK’s createSelector memoized selectors to select only what each component needs.Posts Slice with createSlice
// src/store/postsSlice.js
import { createSlice } from "@reduxjs/toolkit";
const postsSlice = createSlice({
name: "posts",
initialState: {
items: [],
total: 0,
page: 1,
isLoading: false,
error: null,
likedIds: [], // IDs of posts liked by the current user
},
reducers: {
// Synchronous actions — Immer makes mutations safe
setPage(state, action) {
state.page = action.payload;
},
likePost(state, action) {
const postId = action.payload;
const post = state.items.find(p => p.id === postId);
if (post) {
post.like_count += 1;
post.liked_by_me = true;
}
if (!state.likedIds.includes(postId)) {
state.likedIds.push(postId);
}
},
unlikePost(state, action) {
const postId = action.payload;
const post = state.items.find(p => p.id === postId);
if (post) {
post.like_count = Math.max(0, post.like_count - 1);
post.liked_by_me = false;
}
state.likedIds = state.likedIds.filter(id => id !== postId);
},
removePost(state, action) {
state.items = state.items.filter(p => p.id !== action.payload);
state.total -= 1;
},
},
});
// Export actions
export const { setPage, likePost, unlikePost, removePost } = postsSlice.actions;
// Export selectors
export const selectPosts = (s) => s.posts.items;
export const selectPostsTotal = (s) => s.posts.total;
export const selectPostsLoading = (s) => s.posts.isLoading;
export const selectPostsPage = (s) => s.posts.page;
export const selectIsPostLiked = (postId) => (s) => s.posts.likedIds.includes(postId);
export default postsSlice.reducer;
Using the Store in Components
import { useSelector, useDispatch } from "react-redux";
import { likePost, unlikePost } from "@/store/postsSlice";
import { selectPosts, selectIsPostLiked } from "@/store/postsSlice";
function LikeButton({ postId, likeCount }) {
const dispatch = useDispatch();
const isLiked = useSelector(selectIsPostLiked(postId));
function handleToggle() {
dispatch(isLiked ? unlikePost(postId) : likePost(postId));
// Note: this updates local Redux state optimistically.
// The API call is handled separately (e.g., in a thunk or side-effect).
}
return (
<button onClick={handleToggle}
className={isLiked ? "text-red-500" : "text-gray-400"}>
{isLiked ? "♥" : "♡"} {likeCount}
</button>
);
}
function PostFeed() {
const posts = useSelector(selectPosts);
const isLoading = useSelector(selectPostsLoading);
if (isLoading) return <PostListSkeleton />;
return (
<div className="grid gap-4">
{posts.map(p => <PostCard key={p.id} post={p} />)}
</div>
);
}
Common Mistakes
Mistake 1 — Selecting too broadly (unnecessary re-renders)
❌ Wrong — entire state object causes re-render on any change:
const state = useSelector(s => s); // re-renders on ANY state change!
✅ Correct — select the minimal needed slice:
const isLoading = useSelector(s => s.posts.isLoading); // ✓
Mistake 2 — Forgetting to connect the slice reducer to configureStore
❌ Wrong — slice exists but is not registered:
configureStore({ reducer: { auth: authReducer } });
// posts slice not added! s.posts is undefined in components
✅ Correct — add every slice to configureStore.
Quick Reference
| Task | Code |
|---|---|
| Create slice | createSlice({ name, initialState, reducers }) |
| Create store | configureStore({ reducer: { sliceName: sliceReducer } }) |
| Wrap app | <Provider store={store}> |
| Read state | useSelector(s => s.sliceName.field) |
| Dispatch action | dispatch(actionCreator(payload)) |
| Export selectors | export const selectX = s => s.slice.x |