Project Structure
How to organize your Redux Toolkit code for maintainable apps.
Recommended Folder Structure
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
- Feature-based - Group by domain, not file type
- Co-location - Keep related code close together
- Clear boundaries - Separate concerns clearly
- Consistent naming - Follow naming conventions
- 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-caseorcamelCaseconsistently
This structure keeps your Redux code organized and easy to find as your app grows!