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.