Store lifetime patterns by environment
Three main store lifetime patterns exist based on environment: Client-only SPA uses one module-level singleton store because there is one browser session with no cross-request leakage risk. SSR-heavy React apps use makeStore() plus provider-local state because each request needs its own store instance that must stay stable across client renders. Non-React integration code can use direct store access outside the React context boundary and should stay out of UI components.
SPA pattern: module-level singleton store
In a client-only SPA, create a single store using configureStore and export it as a module-level variable. This store instance persists for the lifetime of the browser session and can be accessed from anywhere in the application. Use this pattern for classic browser SPAs.
SPA pattern example with configureStore
Example of module-level singleton store pattern: import { configureStore } from '@reduxjs/toolkit' and import the slice reducer. Export const store = configureStore({ reducer: { posts: postsSlice.reducer } }).
SSR pattern: makeStore factory function
In SSR-heavy React apps, create a makeStore factory function that returns a new configureStore instance. This factory is called once per request to create a fresh store instance. Each request gets its own isolated store to prevent cross-request leakage.
SSR pattern: StoreProvider component
In SSR React apps, create a StoreProvider component marked with 'use client'. Inside, call useState with the makeStore factory as the initializer (not makeStore() directly) to get a stable store instance. Pass this store to the Provider component. This keeps the store instance stable across client renders after SSR.
SSR pattern example with makeStore and StoreProvider
Example SSR pattern: Define makeStore as a factory returning configureStore. In StoreProvider component, use const [store] = useState(makeStore) to initialize store once and keep it stable. Wrap children with <Provider store={store}>{children}</Provider>. Mark StoreProvider with 'use client' directive for Next.js.
Prefer RTK Query over createAsyncThunk for server state
When migrating server-data handling, prefer RTK Query (createApi with fetchBaseQuery) over createAsyncThunk if the feature is really a server cache. RTK Query handles loading status, caching, and fetched data automatically, avoiding the need to manually rebuild the old loading-flag architecture with new APIs.
RTK Query replaces legacy server-data stacks
Use createApi from @reduxjs/toolkit/query/react with fetchBaseQuery for modern server state management instead of legacy patterns. Example: createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), endpoints: (build) => ({ getTodos: build.query<Todo[], void>({ query: () => 'todos' }) }) }).
createApi generates slice reducer and middleware for Redux store
createApi internally calls createSlice to generate a slice reducer and corresponding action creators with logic for caching fetched data. It also automatically generates a custom Redux middleware that manages subscription counts and cache lifetimes.
Keep editable form state local with useState until user commits
Form editing state should be kept in component state using useState rather than Redux. Redux should be used for shared, durable app state, not for every keystroke. Only dispatch to Redux when the user commits the form (e.g., clicks Save), not during intermediate changes.
Keep URL state with the router and combine it at the edge
URL state should remain owned by the router. When you need to combine URL parameters with Redux state, pass the URL parameter into selectors or combine them in the component instead of syncing the URL state into Redux. This avoids creating two sources of truth.
Do not put form editing state in Redux
Putting per-keystroke form editing state in Redux is a mistake. Per-keystroke dispatching adds unnecessary global complexity for data that usually lives in a single component tree. Use local component state with useState instead.
Do not synchronize router or URL state into Redux
Synchronizing router or URL state into Redux creates two sources of truth. URL state already has an authoritative owner in the router, so duplicating it into Redux state is a mistake. Instead, read the URL state directly and pass it to selectors or combine it in the component.
Redux Toolkit is the official recommended approach for writing Redux logic
Redux Toolkit (RTK) is the official recommended approach for writing Redux logic. The @reduxjs/toolkit package wraps around the core redux package and contains API methods and common dependencies essential for building Redux apps. Redux Toolkit builds in suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications. If you are writing any Redux logic today, you should be using Redux Toolkit to write that code.
Redux core APIs: createStore, combineReducers, applyMiddleware, compose
The Redux core is a small and deliberately unopinionated library that provides four small API primitives: createStore to actually create a Redux store, combineReducers to combine multiple slice reducers into a single larger reducer, applyMiddleware to combine multiple middleware into a store enhancer, and compose to combine multiple store enhancers into a single store enhancer.
RTK Query is a full data fetching and caching solution included as optional entry point
RTK Query is a full data fetching and caching solution for Redux apps, included as a separate optional @reduxjs/toolkit/query entry point. It lets you define endpoints for REST, GraphQL, or any async function, and generates a reducer and middleware that fully manage fetching data, updating loading state, and caching results. It also automatically generates React hooks that can be used in components to fetch data.
Redux Toolkit APIs are optional and can be picked and chosen
Each Redux Toolkit API (configureStore, createSlice, createAsyncThunk, createEntityAdapter, createListenerMiddleware, etc.) is completely optional and designed for specific use cases. Users can pick and choose which APIs they actually use in their app, though all are highly recommended to help with their respective tasks.
Redux core package is now considered obsolete
The redux core package is now considered obsolete. All of its APIs are re-exported from @reduxjs/toolkit, and configureStore does everything createStore does but with better default behavior and configurability. Redux maintainers strongly encourage users to switch over to @reduxjs/toolkit and update code to use Redux Toolkit APIs instead. The redux core package still works but should not be used for any new Redux code today.
Redux core provides no helpers or boilerplate reduction
With the Redux core library, all Redux-related logic in your app has to be written entirely by you. There are no helpers to make any of your code easier to write. A reducer function is just a function with no built-in helpers, requiring manual switch statements, action creators, and action type constants.
RTK Query createApi defines endpoints for data fetching
createApi() is the core of RTK Query's functionality. It allows you to define a set of endpoints that describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with one API slice per base URL as a rule of thumb.
fetchBaseQuery wraps fetch for RTK Query
fetchBaseQuery() is a small wrapper around fetch that aims to simplify requests. It is intended as the recommended baseQuery to be used in createApi for the majority of users.
setupListeners enables RTK Query refetch behaviors
setupListeners() is a utility used to enable refetchOnMount and refetchOnReconnect behaviors in RTK Query.
ApiProvider can be used without existing Redux store
ApiProvider is a React component that can be used as a Provider if you do not already have a Redux store.
RTK Query is optional addon with separate entry points
RTK Query is provided as an optional addon within the @reduxjs/toolkit package. It is available via two entry points: @reduxjs/toolkit/query for core functionality, and @reduxjs/toolkit/query/react for React-specific hooks that automatically generate hooks corresponding to defined endpoints.
fakeBaseQuery function
fakeBaseQuery is exported as a public function with signature: <ErrorType>(): BaseQueryFn<void, NEVER, ErrorType, {}>. It returns a no-op base query function for testing or placeholder purposes.
Api.injectEndpoints method
The Api type has an injectEndpoints method with signature: injectEndpoints<NewDefinitions extends EndpointDefinitions>(_: { endpoints: (build: EndpointBuilder<BaseQuery, TagTypes, ReducerPath>) => NewDefinitions, overrideExisting?: boolean }): Api<BaseQuery, Definitions & NewDefinitions, ReducerPath, TagTypes, Enhancers>. It returns a new Api with the injected endpoints merged into the Definitions type parameter.
Api.enhanceEndpoints method
The Api type has an enhanceEndpoints method with signature: enhanceEndpoints<NewTagTypes extends string = never>(_: { addTagTypes?: readonly NewTagTypes[], endpoints?: ReplaceTagTypes<Definitions, TagTypes | NoInfer<NewTagTypes>> extends infer NewDefinitions ? { [K in keyof NewDefinitions]?: Partial<NewDefinitions[K]> | ((definition: NewDefinitions[K]) => void) } : never }): Api<BaseQuery, ReplaceTagTypes<Definitions, TagTypes | NewTagTypes>, ReducerPath, TagTypes | NewTagTypes, Enhancers>. It allows adding new tag types and modifying endpoint definitions.
BaseQueryFn type signature
BaseQueryFn is a generic type with signature: <Args = any, Result = unknown, Error = unknown, DefinitionExtraOptions = { copyWithStructuralSharing?: boolean }, Meta = {}> = (args: Args, api: BaseQueryApi, extraOptions: DefinitionExtraOptions) => MaybePromise<QueryReturnValue<Result, Error, Meta>>. It defines the base query function contract for RTK Query.
BaseQueryEnhancer type
BaseQueryEnhancer is a generic type with signature: <AdditionalArgs = unknown, AdditionalDefinitionExtraOptions = unknown, Config = void> = <BaseQuery extends BaseQueryFn>(baseQuery: BaseQuery, config: Config) => BaseQueryFn<BaseQueryArg<BaseQuery> & AdditionalArgs, BaseQueryResult<BaseQuery>, BaseQueryError<BaseQuery>, BaseQueryExtraOptions<BaseQuery> & AdditionalDefinitionExtraOptions>. It wraps a BaseQueryFn and returns an enhanced version.
buildCreateApi function
buildCreateApi is exported as a public function with signature: <Modules extends [Module<any>, ...Module<any>[]]>(...modules: Modules): CreateApi<Modules[number]['name']>. It takes a list of modules and returns a CreateApi function.
coreModule constant
coreModule is exported as a public constant function with signature: () => Module<CoreModule>. It returns the core module for RTK Query.
createApi constant
createApi is exported as a public constant with type CreateApi<typeof coreModuleName>. It is the main API creation function for RTK Query, using only the core module.
CreateApiOptions interface properties
CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes> has the following required and optional properties: baseQuery (required, BaseQueryFn), endpoints (required, function taking EndpointBuilder and returning Definitions), extractRehydrationInfo (optional, function), keepUnusedDataFor (optional, number), reducerPath (optional, defaults to 'api'), refetchOnFocus (optional, boolean), refetchOnMountOrArgChange (optional, boolean or number), refetchOnReconnect (optional, boolean), serializeQueryArgs (optional, SerializeQueryArgs<unknown>), structuralSharing (optional, boolean), tagTypes (optional, readonly TagTypes[]).
EndpointDefinition union type
EndpointDefinition is a union type that can be either QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> or MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>.
EndpointDefinitions type
EndpointDefinitions is defined as Record<string, EndpointDefinition<any, any, any, any>>, a record mapping string keys to EndpointDefinitions.
FetchArgs interface properties
FetchArgs extends CustomRequestInit and has the following properties: url (required, string), body (optional, any), params (optional, Record<string, any>), responseHandler (optional, ResponseHandler), validateStatus (optional, function taking Response and any, returning boolean).
fetchBaseQuery function
fetchBaseQuery is exported as a public function with signature: ({baseUrl, prepareHeaders, fetchFn, paramsSerializer, ...baseFetchOptions}?: FetchBaseQueryArgs): BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>. It creates a base query function using the Fetch API.
FetchBaseQueryError union type
FetchBaseQueryError is a union of four error types: { status: number, data: unknown } | { status: 'FETCH_ERROR', data?: undefined, error: string } | { status: 'PARSING_ERROR', originalStatus: number, data: string, error: string } | { status: 'CUSTOM_ERROR', data?: unknown, error: string }.
FetchBaseQueryMeta type
FetchBaseQueryMeta is defined as { request: Request, response?: Response }, containing the Request object and optional Response object from a fetch operation.
Module type interface
Module<Name extends ModuleName> has a name property of type Name and an init method with signature: <BaseQuery extends BaseQueryFn, Definitions extends EndpointDefinitions, ReducerPath extends string, TagTypes extends string>(api: Api<BaseQuery, EndpointDefinitions, ReducerPath, TagTypes, ModuleName>, options: WithRequiredProp<CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes>, 'reducerPath' | 'serializeQueryArgs' | 'keepUnusedDataFor' | 'refetchOnMountOrArgChange' | 'refetchOnFocus' | 'refetchOnReconnect' | 'tagTypes' | 'structuralSharing'>, context: ApiContext<Definitions>): { injectEndpoint(endpointName: string, definition: EndpointDefinition<any, any, any, any>): void }.
MutationDefinition type
MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> combines BaseEndpointDefinition<QueryArg, BaseQuery, ResultType> with MutationExtraOptions<TagTypes, ResultType, QueryArg, BaseQuery, ReducerPath>.
QueryDefinition type
QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> combines BaseEndpointDefinition<QueryArg, BaseQuery, ResultType> with QueryExtraOptions<TagTypes, ResultType, QueryArg, BaseQuery, ReducerPath>.
QueryStatus enum values
QueryStatus is an enum with four values: fulfilled = 'fulfilled', pending = 'pending', rejected = 'rejected', uninitialized = 'uninitialized'.
retry BaseQueryEnhancer
retry is exported as a public constant of type BaseQueryEnhancer<unknown, RetryOptions, void | RetryOptions> with a fail property of type typeof fail_2. It provides retry logic for base queries.