Store (Saga template)
What is the Store?
The store is the single source of truth for application state. It holds the combined reducer, the middleware chain, and the dispatcher. In the saga template the store is also the seam where the saga middleware gets .run(rootSaga) so every slice's watcher starts listening at boot.
Key Principles
One store, period. No nested stores, no per-feature stores. Every slice lives under
combineReducers({...})and is reached throughuseSelector((state) => state.<slice>.…).createStorefromredux(notconfigureStore). This template stays on plain redux + redux-saga + the devtools extension. RTK'sconfigureStorewould compose middleware differently and conflict withcomposeWithDevTools(applyMiddleware(...)).Saga middleware is created first, run last. Order matters:
const sagaMiddleware = createSagaMiddleware()→ wrap withapplyMiddleware(sagaMiddleware)→ enhance withcomposeWithDevTools(...enhancers)→createStore(reducer, composedEnhancers)→ finallysagaMiddleware.run(rootSaga). Calling.run()beforecreateStoreis a runtime error.Store is provided once, at the App root.
<Provider store={store}>wraps the React tree inApp.jsx. Containers reach state throughuseSelectorand dispatch withuseDispatch. Nothing else importsstoredirectly.DevTools opt-in.
composeWithDevToolsis a no-op when the extension isn't installed, so it's safe to ship in production builds without conditional code.
Best Practices
✅ DO:
- Keep
rootReducer.jsto a singlecombineReducers({ ... })call. Add/* istanbul ignore file */so coverage doesn't penalise the wiring file. - Keep
index.js(the store) under ~25 lines. It does four things: import, create middleware, compose, run. - Add slices to
combineReducersin alphabetical order; add watchers torootSagasall([...])in the same order. Easier diffing as the app grows.
❌ DON'T:
- Don't
applyMiddleware(thunk, sagaMiddleware)— pick one async strategy. The saga template chose sagas. - Don't lazy-load slices into the store. Reducer composition must be static at boot so the saga's
select(state => state.x)is safe. - Don't run sagas in tests by reaching into the production store. Use
runSagafromredux-sagato drive workers in isolation.
Code Patterns
src/state/index.js
import { composeWithDevTools } from '@redux-devtools/extension'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from "redux-saga";
import reducer from "./rootReducer";
import sagas from "./rootSagas";
// 1. create the saga middleware
const sagaMiddleware = createSagaMiddleware();
// 2. mount it on the Store
const enhancer = applyMiddleware(sagaMiddleware)
const enhancers = [enhancer];
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(reducer, composedEnhancers);
// 3. then run the saga (after createStore!)
sagaMiddleware.run(sagas);
export default store;
src/state/rootReducer.js
/* istanbul ignore file */
import { combineReducers } from "redux";
import todo from "./todo/todo.reducer";
import filters from "./filters/filters.reducer";
import config from "./config/config.reducer";
export default combineReducers({
todo,
filters,
config,
});
Provider wiring at the app root (src/App.jsx)
import { Provider } from "react-redux";
import store from "./state";
import HomePage from "./pages";
export default function App() {
return (
<Provider store={store}>
<HomePage />
</Provider>
);
}
Reading and dispatching from a container
import { useSelector, useDispatch } from "react-redux";
import { readTodo } from "../state/todo/todo.actions";
const todos = useSelector((state) => state.todo.todoItems);
const dispatch = useDispatch();
dispatch(readTodo()); // saga middleware sees this, runs the worker
Related Terminologies
- Reducer — slices are wired through
combineReducershere. - Saga —
rootSagas.jsis run by this file. - Middleware — sagas ARE middleware; this file is where they get applied.
- DevTools —
composeWithDevToolsis the wiring point.
Quality Gates
-
createStoreis called once; the result is the default export. -
sagaMiddleware.run(sagas)is called AFTERcreateStore. -
combineReducersis the only thing rootReducer.js does. - No
configureStore, no thunk middleware, no async logic inindex.js. - Provider wraps the whole tree; no nested
<Provider>.
Source: templates/chota-react-saga/src/state/index.js, rootReducer.js