Skip to main content

Project Structure

How to organize your Redux Toolkit code for maintainable apps.

src/
├── store/
│ ├── index.js # Store setup
│ └── middleware.js # Custom middleware
├── features/
│ ├── auth/
│ │ ├── authSlice.js # Auth state
│ │ ├── authAPI.js # Auth API calls
│ │ └── index.js # Export everything
│ ├── posts/
│ │ ├── postsSlice.js
│ │ ├── postsAPI.js
│ │ └── index.js
│ └── users/
│ ├── usersSlice.js
│ ├── usersAPI.js
│ └── index.js
├── hooks/
│ ├── useAppDispatch.js # Typed dispatch
│ └── useAppSelector.js # Typed selector
└── components/
├── Auth/
├── Posts/
└── Users/

Feature-Based Organization

Group by what the code does, not what type of file it is.

Good ✅

features/
├── auth/
│ ├── authSlice.js
│ ├── authAPI.js
│ └── components/
├── posts/
│ ├── postsSlice.js
│ ├── postsAPI.js
│ └── components/

Bad ❌

redux/
├── actions/
├── reducers/
├── selectors/
└── middleware/

Store Setup

// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import { authSlice } from '../features/auth'
import { postsSlice } from '../features/posts'
import { usersSlice } from '../features/users'

export const store = configureStore({
reducer: {
auth: authSlice.reducer,
posts: postsSlice.reducer,
users: usersSlice.reducer,
},
// Add middleware if needed
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware(),
})

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

Feature Module Pattern

Each feature exports everything from one place:

// features/posts/index.js
export { default as postsSlice } from './postsSlice'
export { postsAPI } from './postsAPI'
export * from './postsSlice'
export * from './postsAPI'

Slice Structure

Keep slices focused on one domain:

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

// Async actions
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
// API call logic
}
)

// Slice
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
status: 'idle',
error: null,
selectedPost: null,
},
reducers: {
selectPost: (state, action) => {
state.selectedPost = action.payload
},
clearError: (state) => {
state.error = null
},
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.fulfilled, (state, action) => {
state.items = action.payload
})
},
})

export const { selectPost, clearError } = postsSlice.actions
export default postsSlice

API Layer Separation

Keep API logic separate from state logic:

// features/posts/postsAPI.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const postsAPI = createApi({
reducerPath: 'postsAPI',
baseQuery: fetchBaseQuery({
baseUrl: '/api/posts',
}),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '',
providesTags: ['Post'],
}),
addPost: builder.mutation({
query: (post) => ({
url: '',
method: 'POST',
body: post,
}),
invalidatesTags: ['Post'],
}),
}),
})

export const { useGetPostsQuery, useAddPostMutation } = postsAPI

Typed Hooks

Create typed versions of useSelector and useDispatch:

// hooks/useAppSelector.js
import { TypedUseSelectorHook, useSelector } from 'react-redux'
import type { RootState } from '../store'

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
// hooks/useAppDispatch.js
import { useDispatch } from 'react-redux'
import type { AppDispatch } from '../store'

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

Selectors Organization

Create reusable selectors:

// features/posts/selectors.js
import { createSelector } from '@reduxjs/toolkit'

// Basic selectors
export const selectPosts = (state) => state.posts.items
export const selectPostsStatus = (state) => state.posts.status
export const selectSelectedPost = (state) => state.posts.selectedPost

// Memoized selectors
export const selectPublishedPosts = createSelector(
[selectPosts],
(posts) => posts.filter(post => post.published)
)

export const selectPostById = createSelector(
[selectPosts, (state, postId) => postId],
(posts, postId) => posts.find(post => post.id === postId)
)

Large App Structure

For bigger apps, organize by domain:

src/
├── app/
│ ├── store.js
│ └── App.js
├── shared/
│ ├── api/
│ ├── utils/
│ └── components/
└── domains/
├── user-management/
│ ├── store/
│ ├── components/
│ └── pages/
├── content-management/
│ ├── store/
│ ├── components/
│ └── pages/
└── analytics/
├── store/
├── components/
└── pages/

Environment-Specific Config

// store/index.js
import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => {
const middleware = getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['persist/PERSIST'],
},
})

// Add development-only middleware
if (process.env.NODE_ENV === 'development') {
const { logger } = require('redux-logger')
middleware.concat(logger)
}

return middleware
},
devTools: process.env.NODE_ENV !== 'production',
})

Constants Management

Keep action types and other constants organized:

// features/posts/constants.js
export const POST_STATUS = {
IDLE: 'idle',
LOADING: 'loading',
SUCCEEDED: 'succeeded',
FAILED: 'failed',
}

export const POST_TYPES = {
DRAFT: 'draft',
PUBLISHED: 'published',
ARCHIVED: 'archived',
}

Testing Structure

Organize tests alongside code:

features/
├── posts/
│ ├── postsSlice.js
│ ├── postsSlice.test.js
│ ├── postsAPI.js
│ ├── postsAPI.test.js
│ └── __tests__/
│ └── integration.test.js

Key Principles

  1. Feature-based - Group by domain, not file type
  2. Co-location - Keep related code close together
  3. Clear boundaries - Separate concerns clearly
  4. Consistent naming - Follow naming conventions
  5. Scalable - Structure grows with your app

Naming Conventions

  • Slices: featureSlice.js (e.g., authSlice.js)
  • APIs: featureAPI.js (e.g., postsAPI.js)
  • Actions: verbNoun (e.g., fetchPosts, updateUser)
  • Selectors: selectThing (e.g., selectPosts, selectUser)
  • Files: kebab-case or camelCase consistently

This structure keeps your Redux code organized and easy to find as your app grows!