State Management08-01-202512 min read

Redux Toolkit Best Practices for Modern React Applications

A practical guide to structuring Redux Toolkit logic, slices, selectors, and async flows for large-scale frontend applications.

Redux Toolkit Best Practices for Modern React Applications

Redux Toolkit solves the biggest pain points of classic Redux by giving you a predictable, opinionated structure with almost no boilerplate. This guide focuses on practical patterns used in serious production apps, especially those with complex client-side logic.

1. Structure the Store by Feature

Organize your app by domain, not by file type.

Good structure:

src/
  features/
    auth/
      authSlice.ts
      authSelectors.ts
      authThunks.ts
    cart/
      cartSlice.ts
      cartSelectors.ts
      cartThunks.ts
  store.ts

Each folder owns its entire feature domain.

2. Use Slices as the Main State Unit

Slices keep actions, reducers, and state in one place.

const authSlice = createSlice({
  name: "auth",
  initialState: { user: null, status: "idle" },
  reducers: {
    logout(state) {
      state.user = null
    }
  }
})

This keeps logic predictable and self-contained.

3. Async Logic Belongs in Thunks

Do not mix async logic with components.

export const loginUser = createAsyncThunk(
  "auth/loginUser",
  async (credentials) => {
    const response = await api.login(credentials)
    return response.data
  }
)

Components stay clean:

dispatch(loginUser({ email, password }))

4. Handle Loading and Error States Consistently

Use extraReducers to manage transition states.

extraReducers: (builder) => {
  builder
    .addCase(loginUser.pending, (state) => {
      state.status = "loading"
    })
    .addCase(loginUser.fulfilled, (state, action) => {
      state.status = "success"
      state.user = action.payload
    })
    .addCase(loginUser.rejected, (state) => {
      state.status = "error"
    })
}

Every async flow gets a predictable state machine.

5. Use Selectors to Encapsulate Derived Logic

Selectors prevent duplication and improve performance.

export const selectIsLoggedIn = (state) => Boolean(state.auth.user)
export const selectUserRole = (state) => state.auth.user?.role

Component code becomes simpler and more testable.

6. Avoid Excess Normalization

Normalize only when necessary for large collections or complex updates.

Redux Toolkit includes createEntityAdapter for this use case.

7. Use useSelector Carefully

Avoid subscribing to the entire slice.

Bad:

const auth = useSelector((state) => state.auth)

Good:

const user = useSelector(selectUser)

This reduces unnecessary rerenders.

8. Keep UI State Out of Redux

Examples that should not be in Redux:

  • modal open states
  • form field values
  • dropdown visibility
  • simple toggles

Use component state or Zustand for UI-level state.

9. Use Immer Mutations

Redux Toolkit uses Immer under the hood. Write updates like normal JS mutations.

state.items.push(newItem)

This is safe and improves readability.

10. Combine Redux Toolkit with React Query

Use Redux for:

  • client-only state
  • business logic
  • workflows

Use React Query for:

  • server data
  • caching
  • synchronization

Each tool stays in its lane.

11. Add Strong TypeScript Support

Use typed dispatch and selectors.

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Typed hooks:

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector = useSelector

Final Thoughts

Redux Toolkit remains one of the best solutions for complex client-side logic. When combined with clear domain boundaries, predictable async flows, and strong typing, it becomes a powerful and maintainable state architecture for large frontend applications.