createSlice and Store Setup — Writing Modern Redux

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

🧠 Test Yourself

A component uses useSelector(s => s.posts). A completely unrelated auth action is dispatched. Does the component re-render?