@depup/reduxjs__toolkit 2.12.0-depup.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/changes.json +18 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/redux-toolkit.development.cjs +2406 -0
- package/dist/cjs/redux-toolkit.development.cjs.map +1 -0
- package/dist/cjs/redux-toolkit.production.min.cjs +3 -0
- package/dist/cjs/redux-toolkit.production.min.cjs.map +1 -0
- package/dist/index.d.mts +2665 -0
- package/dist/index.d.ts +2665 -0
- package/dist/query/cjs/index.js +6 -0
- package/dist/query/cjs/rtk-query.development.cjs +3092 -0
- package/dist/query/cjs/rtk-query.development.cjs.map +1 -0
- package/dist/query/cjs/rtk-query.production.min.cjs +2 -0
- package/dist/query/cjs/rtk-query.production.min.cjs.map +1 -0
- package/dist/query/index.d.mts +3066 -0
- package/dist/query/index.d.ts +3066 -0
- package/dist/query/react/cjs/index.js +6 -0
- package/dist/query/react/cjs/rtk-query-react.development.cjs +748 -0
- package/dist/query/react/cjs/rtk-query-react.development.cjs.map +1 -0
- package/dist/query/react/cjs/rtk-query-react.production.min.cjs +2 -0
- package/dist/query/react/cjs/rtk-query-react.production.min.cjs.map +1 -0
- package/dist/query/react/index.d.mts +1009 -0
- package/dist/query/react/index.d.ts +1009 -0
- package/dist/query/react/rtk-query-react.browser.mjs +2 -0
- package/dist/query/react/rtk-query-react.browser.mjs.map +1 -0
- package/dist/query/react/rtk-query-react.legacy-esm.js +740 -0
- package/dist/query/react/rtk-query-react.legacy-esm.js.map +1 -0
- package/dist/query/react/rtk-query-react.modern.mjs +705 -0
- package/dist/query/react/rtk-query-react.modern.mjs.map +1 -0
- package/dist/query/rtk-query.browser.mjs +2 -0
- package/dist/query/rtk-query.browser.mjs.map +1 -0
- package/dist/query/rtk-query.legacy-esm.js +3117 -0
- package/dist/query/rtk-query.legacy-esm.js.map +1 -0
- package/dist/query/rtk-query.modern.mjs +3052 -0
- package/dist/query/rtk-query.modern.mjs.map +1 -0
- package/dist/react/cjs/index.js +6 -0
- package/dist/react/cjs/redux-toolkit-react.development.cjs +55 -0
- package/dist/react/cjs/redux-toolkit-react.development.cjs.map +1 -0
- package/dist/react/cjs/redux-toolkit-react.production.min.cjs +2 -0
- package/dist/react/cjs/redux-toolkit-react.production.min.cjs.map +1 -0
- package/dist/react/index.d.mts +22 -0
- package/dist/react/index.d.ts +22 -0
- package/dist/react/redux-toolkit-react.browser.mjs +2 -0
- package/dist/react/redux-toolkit-react.browser.mjs.map +1 -0
- package/dist/react/redux-toolkit-react.legacy-esm.js +47 -0
- package/dist/react/redux-toolkit-react.legacy-esm.js.map +1 -0
- package/dist/react/redux-toolkit-react.modern.mjs +28 -0
- package/dist/react/redux-toolkit-react.modern.mjs.map +1 -0
- package/dist/redux-toolkit.browser.mjs +3 -0
- package/dist/redux-toolkit.browser.mjs.map +1 -0
- package/dist/redux-toolkit.legacy-esm.js +2351 -0
- package/dist/redux-toolkit.legacy-esm.js.map +1 -0
- package/dist/redux-toolkit.modern.mjs +2330 -0
- package/dist/redux-toolkit.modern.mjs.map +1 -0
- package/dist/uncheckedindexed.ts +16 -0
- package/package.json +310 -0
- package/query/package.json +13 -0
- package/query/react/package.json +13 -0
- package/react/package.json +13 -0
- package/skills/build-modern-redux-apps/modern-redux/SKILL.md +304 -0
- package/skills/build-modern-redux-apps/modern-redux/references/store-lifetime.md +53 -0
- package/skills/build-modern-redux-apps/redux-dataflow/SKILL.md +264 -0
- package/skills/evolve-and-diagnose-redux-apps/debug-redux-toolkit-apps/SKILL.md +269 -0
- package/skills/evolve-and-diagnose-redux-apps/migrate-to-modern-redux/SKILL.md +226 -0
- package/skills/manage-server-data/adopt-rtk-query/SKILL.md +385 -0
- package/skills/manage-server-data/adopt-rtk-query/references/endpoint-lifecycle.md +36 -0
- package/skills/model-redux-state/build-slices-and-selectors/SKILL.md +364 -0
- package/skills/model-redux-state/build-slices-and-selectors/references/slice-patterns.md +59 -0
- package/skills/model-redux-state/design-state-ownership/SKILL.md +322 -0
- package/skills/model-redux-state/design-state-ownership/references/state-ownership.md +28 -0
- package/skills/orchestrate-side-effects/handle-side-effects/SKILL.md +271 -0
- package/skills/orchestrate-side-effects/handle-side-effects/references/listener-workflows.md +34 -0
- package/src/actionCreatorInvariantMiddleware.ts +34 -0
- package/src/autoBatchEnhancer.ts +146 -0
- package/src/combineSlices.ts +487 -0
- package/src/configureStore.ts +248 -0
- package/src/createAction.ts +324 -0
- package/src/createAsyncThunk.ts +791 -0
- package/src/createDraftSafeSelector.ts +30 -0
- package/src/createReducer.ts +217 -0
- package/src/createSlice.ts +1079 -0
- package/src/devtoolsExtension.ts +241 -0
- package/src/dynamicMiddleware/index.ts +93 -0
- package/src/dynamicMiddleware/react/index.ts +101 -0
- package/src/dynamicMiddleware/types.ts +82 -0
- package/src/entities/create_adapter.ts +47 -0
- package/src/entities/entity_state.ts +38 -0
- package/src/entities/index.ts +8 -0
- package/src/entities/models.ts +198 -0
- package/src/entities/sorted_state_adapter.ts +266 -0
- package/src/entities/state_adapter.ts +58 -0
- package/src/entities/state_selectors.ts +73 -0
- package/src/entities/unsorted_state_adapter.ts +204 -0
- package/src/entities/utils.ts +68 -0
- package/src/formatProdErrorMessage.ts +13 -0
- package/src/getDefaultEnhancers.ts +31 -0
- package/src/getDefaultMiddleware.ts +113 -0
- package/src/immerImports.ts +7 -0
- package/src/immutableStateInvariantMiddleware.ts +274 -0
- package/src/index.ts +213 -0
- package/src/listenerMiddleware/exceptions.ts +20 -0
- package/src/listenerMiddleware/index.ts +562 -0
- package/src/listenerMiddleware/task.ts +100 -0
- package/src/listenerMiddleware/types.ts +886 -0
- package/src/listenerMiddleware/utils.ts +30 -0
- package/src/mapBuilders.ts +298 -0
- package/src/matchers.ts +365 -0
- package/src/nanoid.ts +20 -0
- package/src/query/HandledError.ts +6 -0
- package/src/query/apiTypes.ts +120 -0
- package/src/query/baseQueryTypes.ts +101 -0
- package/src/query/core/apiState.ts +377 -0
- package/src/query/core/buildInitiate.ts +596 -0
- package/src/query/core/buildMiddleware/batchActions.ts +207 -0
- package/src/query/core/buildMiddleware/cacheCollection.ts +204 -0
- package/src/query/core/buildMiddleware/cacheLifecycle.ts +379 -0
- package/src/query/core/buildMiddleware/devMiddleware.ts +34 -0
- package/src/query/core/buildMiddleware/index.ts +162 -0
- package/src/query/core/buildMiddleware/invalidationByTags.ts +139 -0
- package/src/query/core/buildMiddleware/polling.ts +187 -0
- package/src/query/core/buildMiddleware/queryLifecycle.ts +508 -0
- package/src/query/core/buildMiddleware/types.ts +173 -0
- package/src/query/core/buildMiddleware/windowEventHandling.ts +64 -0
- package/src/query/core/buildSelectors.ts +413 -0
- package/src/query/core/buildSlice.ts +735 -0
- package/src/query/core/buildThunks.ts +1124 -0
- package/src/query/core/index.ts +58 -0
- package/src/query/core/module.ts +727 -0
- package/src/query/core/rtkImports.ts +24 -0
- package/src/query/core/setupListeners.ts +118 -0
- package/src/query/createApi.ts +506 -0
- package/src/query/defaultSerializeQueryArgs.ts +52 -0
- package/src/query/endpointDefinitions.ts +1717 -0
- package/src/query/fakeBaseQuery.ts +21 -0
- package/src/query/fetchBaseQuery.ts +385 -0
- package/src/query/index.ts +105 -0
- package/src/query/react/ApiProvider.tsx +69 -0
- package/src/query/react/buildHooks.ts +2297 -0
- package/src/query/react/constants.ts +2 -0
- package/src/query/react/index.ts +45 -0
- package/src/query/react/module.ts +275 -0
- package/src/query/react/namedHooks.ts +59 -0
- package/src/query/react/reactImports.ts +10 -0
- package/src/query/react/reactReduxImports.ts +1 -0
- package/src/query/react/rtkqImports.ts +8 -0
- package/src/query/react/useSerializedStableValue.ts +17 -0
- package/src/query/react/useShallowStableValue.ts +13 -0
- package/src/query/retry.ts +233 -0
- package/src/query/standardSchema.ts +35 -0
- package/src/query/tsHelpers.ts +48 -0
- package/src/query/utils/capitalize.ts +3 -0
- package/src/query/utils/copyWithStructuralSharing.ts +27 -0
- package/src/query/utils/countObjectKeys.ts +14 -0
- package/src/query/utils/filterMap.ts +27 -0
- package/src/query/utils/getCurrent.ts +6 -0
- package/src/query/utils/getOrInsert.ts +41 -0
- package/src/query/utils/immerImports.ts +9 -0
- package/src/query/utils/index.ts +11 -0
- package/src/query/utils/isAbsoluteUrl.ts +9 -0
- package/src/query/utils/isDocumentVisible.ts +12 -0
- package/src/query/utils/isNotNullish.ts +7 -0
- package/src/query/utils/isOnline.ts +12 -0
- package/src/query/utils/isValidUrl.ts +9 -0
- package/src/query/utils/joinUrls.ts +26 -0
- package/src/query/utils/signals.ts +33 -0
- package/src/react/index.ts +7 -0
- package/src/reduxImports.ts +8 -0
- package/src/reselectImports.ts +1 -0
- package/src/serializableStateInvariantMiddleware.ts +285 -0
- package/src/tsHelpers.ts +214 -0
- package/src/uncheckedindexed.ts +16 -0
- package/src/utils.ts +125 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: evolve-and-diagnose-redux-apps/migrate-to-modern-redux
|
|
3
|
+
description: >
|
|
4
|
+
Use this when moving a legacy Redux codebase to current RTK patterns. Covers
|
|
5
|
+
replacing createStore with configureStore, migrating touched reducers to
|
|
6
|
+
createSlice, codemod-assisted RTK 2 updates, and replacing server-data stacks
|
|
7
|
+
with RTK Query instead of writing new legacy Redux code.
|
|
8
|
+
type: lifecycle
|
|
9
|
+
library: "@reduxjs/toolkit"
|
|
10
|
+
library_version: "2.11.2"
|
|
11
|
+
requires:
|
|
12
|
+
- build-modern-redux-apps/modern-redux
|
|
13
|
+
sources:
|
|
14
|
+
- "reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx"
|
|
15
|
+
- "reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md"
|
|
16
|
+
- "reduxjs/redux-toolkit:packages/rtk-codemods/README.md"
|
|
17
|
+
- "reduxjs/redux:docs/style-guide/style-guide.md"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Migrate To Modern Redux
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// before
|
|
26
|
+
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
|
27
|
+
import thunk from 'redux-thunk'
|
|
28
|
+
|
|
29
|
+
const postsReducer = (state = [] as { id: string; title: string }[]) => state
|
|
30
|
+
const usersReducer = (state = [] as { id: string; name: string }[]) => state
|
|
31
|
+
|
|
32
|
+
const rootReducer = combineReducers({
|
|
33
|
+
posts: postsReducer,
|
|
34
|
+
users: usersReducer,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const legacyStore = createStore(rootReducer, applyMiddleware(thunk))
|
|
38
|
+
|
|
39
|
+
// after
|
|
40
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
41
|
+
|
|
42
|
+
const postsReducer = (state = [] as { id: string; title: string }[]) => state
|
|
43
|
+
const usersReducer = (state = [] as { id: string; name: string }[]) => state
|
|
44
|
+
|
|
45
|
+
export const store = configureStore({
|
|
46
|
+
reducer: {
|
|
47
|
+
posts: postsReducer,
|
|
48
|
+
users: usersReducer,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Core Patterns
|
|
54
|
+
|
|
55
|
+
### Replace the store setup first
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
59
|
+
|
|
60
|
+
const postsReducer = (state = [] as { id: string; title: string }[]) => state
|
|
61
|
+
const usersReducer = (state = [] as { id: string; name: string }[]) => state
|
|
62
|
+
|
|
63
|
+
export const store = configureStore({
|
|
64
|
+
reducer: {
|
|
65
|
+
posts: postsReducer,
|
|
66
|
+
users: usersReducer,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export type RootState = ReturnType<typeof store.getState>
|
|
71
|
+
export type AppDispatch = typeof store.dispatch
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This is the one migration step that can happen immediately while old reducers continue to work.
|
|
75
|
+
|
|
76
|
+
### Migrate reducers as you touch them
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { createSlice } from '@reduxjs/toolkit'
|
|
80
|
+
|
|
81
|
+
type TodosState = {
|
|
82
|
+
items: { id: string; text: string; completed: boolean }[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const initialState: TodosState = {
|
|
86
|
+
items: [],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const todosSlice = createSlice({
|
|
90
|
+
name: 'todos',
|
|
91
|
+
initialState,
|
|
92
|
+
reducers: {
|
|
93
|
+
todoAdded(state, action: { payload: { id: string; text: string } }) {
|
|
94
|
+
state.items.push({ ...action.payload, completed: false })
|
|
95
|
+
},
|
|
96
|
+
todoToggled(state, action: { payload: { id: string } }) {
|
|
97
|
+
const todo = state.items.find((item) => item.id === action.payload.id)
|
|
98
|
+
if (todo) {
|
|
99
|
+
todo.completed = !todo.completed
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Once a reducer needs editing, migrate that reducer instead of adding more legacy code to it.
|
|
107
|
+
|
|
108
|
+
### Use codemods for mechanical updates
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx @reduxjs/rtk-codemods createSliceBuilder src/features/posts/postsSlice.ts
|
|
112
|
+
npx @reduxjs/rtk-codemods createReducerBuilder src/features/posts/postsReducer.ts
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use codemods for repetitive RTK API migrations, then review the result and finish the semantic cleanup by hand.
|
|
116
|
+
|
|
117
|
+
### Replace legacy server-data stacks with RTK Query
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
121
|
+
|
|
122
|
+
type Todo = { id: string; text: string }
|
|
123
|
+
|
|
124
|
+
export const api = createApi({
|
|
125
|
+
reducerPath: 'api',
|
|
126
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
127
|
+
endpoints: (build) => ({
|
|
128
|
+
getTodos: build.query<Todo[], void>({
|
|
129
|
+
query: () => 'todos',
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When the old code is just request status plus fetched data, migrate toward RTK Query instead of carrying the thunk stack forward forever.
|
|
136
|
+
|
|
137
|
+
## Common Mistakes
|
|
138
|
+
|
|
139
|
+
### HIGH Attempting a big-bang rewrite
|
|
140
|
+
|
|
141
|
+
Wrong:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Replace every reducer, every connected component, and every async flow
|
|
145
|
+
// in one branch before shipping anything.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Correct:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// 1) Switch createStore to configureStore
|
|
152
|
+
// 2) Migrate one touched reducer to createSlice
|
|
153
|
+
// 3) Convert touched connected components to hooks
|
|
154
|
+
// 4) Repeat without introducing new legacy Redux code
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Modern Redux migration is incremental, but once the store is modernized new work should stop adding legacy patterns.
|
|
158
|
+
|
|
159
|
+
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx
|
|
160
|
+
|
|
161
|
+
### CRITICAL Carrying removed RTK 2 config forms forward
|
|
162
|
+
|
|
163
|
+
Wrong:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
configureStore({
|
|
167
|
+
reducer,
|
|
168
|
+
middleware: [logger],
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Correct:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
configureStore({
|
|
176
|
+
reducer,
|
|
177
|
+
middleware: (getDefaultMiddleware) =>
|
|
178
|
+
getDefaultMiddleware().concat(logger),
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
RTK 2 removed array middleware configuration and other older builder forms that agents trained on RTK 1.x still emit.
|
|
183
|
+
|
|
184
|
+
Source: reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md
|
|
185
|
+
|
|
186
|
+
### HIGH Preserving hand-written fetch state by default
|
|
187
|
+
|
|
188
|
+
Wrong:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { createAsyncThunk } from '@reduxjs/toolkit'
|
|
192
|
+
|
|
193
|
+
type Todo = { id: string; text: string }
|
|
194
|
+
|
|
195
|
+
const initialState = {
|
|
196
|
+
items: [] as Todo[],
|
|
197
|
+
status: 'idle' as 'idle' | 'pending' | 'failed',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
|
|
201
|
+
const response = await fetch('/api/todos')
|
|
202
|
+
return (await response.json()) as Todo[]
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Correct:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
210
|
+
|
|
211
|
+
type Todo = { id: string; text: string }
|
|
212
|
+
|
|
213
|
+
const api = createApi({
|
|
214
|
+
reducerPath: 'api',
|
|
215
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
216
|
+
endpoints: (build) => ({
|
|
217
|
+
getTodos: build.query<Todo[], void>({
|
|
218
|
+
query: () => 'todos',
|
|
219
|
+
}),
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
If the feature is really server cache, keep the migration moving toward RTK Query instead of rebuilding the old loading-flag architecture in new APIs.
|
|
225
|
+
|
|
226
|
+
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-server-data/adopt-rtk-query
|
|
3
|
+
description: >
|
|
4
|
+
Use this when adding RTK Query as the default server-data and document-cache
|
|
5
|
+
layer. Covers createApi, store integration, hooks, invalidation behavior,
|
|
6
|
+
optimistic updates, and deciding when RTK Query is the right cache model.
|
|
7
|
+
type: lifecycle
|
|
8
|
+
library: "@reduxjs/toolkit"
|
|
9
|
+
library_version: "2.11.2"
|
|
10
|
+
requires:
|
|
11
|
+
- build-modern-redux-apps/modern-redux
|
|
12
|
+
sources:
|
|
13
|
+
- "reduxjs/redux-toolkit:docs/rtk-query/api/createApi.mdx"
|
|
14
|
+
- "reduxjs/redux-toolkit:docs/rtk-query/usage/automated-refetching.mdx"
|
|
15
|
+
- "reduxjs/redux-toolkit:docs/rtk-query/usage/manual-cache-updates.mdx"
|
|
16
|
+
- "reduxjs/redux-toolkit:docs/rtk-query/usage/persistence-and-rehydration.mdx"
|
|
17
|
+
- "reduxjs/redux-toolkit:docs/tutorials/rtk-query.mdx"
|
|
18
|
+
- "reduxjs/redux:docs/style-guide/style-guide.md"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Adopt RTK Query
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// file: src/services/api.ts
|
|
27
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
28
|
+
|
|
29
|
+
type Post = { id: string; title: string }
|
|
30
|
+
|
|
31
|
+
export const api = createApi({
|
|
32
|
+
reducerPath: 'api',
|
|
33
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
34
|
+
tagTypes: ['Post'],
|
|
35
|
+
endpoints: (build) => ({
|
|
36
|
+
getPosts: build.query<Post[], void>({
|
|
37
|
+
query: () => 'posts',
|
|
38
|
+
providesTags: (result) =>
|
|
39
|
+
result
|
|
40
|
+
? [...result.map(({ id }) => ({ type: 'Post' as const, id })), 'Post']
|
|
41
|
+
: ['Post'],
|
|
42
|
+
}),
|
|
43
|
+
addPost: build.mutation<Post, Pick<Post, 'title'>>({
|
|
44
|
+
query: (body) => ({
|
|
45
|
+
url: 'posts',
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body,
|
|
48
|
+
}),
|
|
49
|
+
invalidatesTags: ['Post'],
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export const { useGetPostsQuery, useAddPostMutation } = api
|
|
55
|
+
|
|
56
|
+
// file: src/app/store.ts
|
|
57
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
58
|
+
import { api } from '../services/api'
|
|
59
|
+
|
|
60
|
+
export const store = configureStore({
|
|
61
|
+
reducer: {
|
|
62
|
+
[api.reducerPath]: api.reducer,
|
|
63
|
+
},
|
|
64
|
+
middleware: (getDefaultMiddleware) =>
|
|
65
|
+
getDefaultMiddleware().concat(api.middleware),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// file: src/App.tsx
|
|
69
|
+
import { Provider } from 'react-redux'
|
|
70
|
+
import { store } from './app/store'
|
|
71
|
+
import { useAddPostMutation, useGetPostsQuery } from './services/api'
|
|
72
|
+
|
|
73
|
+
function Posts() {
|
|
74
|
+
const { data: posts = [] } = useGetPostsQuery()
|
|
75
|
+
const [addPost] = useAddPostMutation()
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<button onClick={() => addPost({ title: 'Write docs' })}>Add</button>
|
|
80
|
+
<ul>
|
|
81
|
+
{posts.map((post) => (
|
|
82
|
+
<li key={post.id}>{post.title}</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function App() {
|
|
90
|
+
return (
|
|
91
|
+
<Provider store={store}>
|
|
92
|
+
<Posts />
|
|
93
|
+
</Provider>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Core Patterns
|
|
99
|
+
|
|
100
|
+
### Keep one API slice per base URL and extend it
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
104
|
+
|
|
105
|
+
export const api = createApi({
|
|
106
|
+
reducerPath: 'api',
|
|
107
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
108
|
+
endpoints: () => ({}),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
export const postsApi = api.injectEndpoints({
|
|
112
|
+
endpoints: (build) => ({
|
|
113
|
+
getPosts: build.query<{ id: string; title: string }[], void>({
|
|
114
|
+
query: () => 'posts',
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Split files with `injectEndpoints`, not by making multiple `createApi` roots for the same backend.
|
|
121
|
+
|
|
122
|
+
### Use tags for cache invalidation
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
type Post = { id: string; title: string }
|
|
126
|
+
|
|
127
|
+
export const api = createApi({
|
|
128
|
+
reducerPath: 'api',
|
|
129
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
130
|
+
tagTypes: ['Post'],
|
|
131
|
+
endpoints: (build) => ({
|
|
132
|
+
getPosts: build.query<Post[], void>({
|
|
133
|
+
query: () => 'posts',
|
|
134
|
+
providesTags: (result) =>
|
|
135
|
+
result
|
|
136
|
+
? [...result.map(({ id }) => ({ type: 'Post' as const, id })), 'Post']
|
|
137
|
+
: ['Post'],
|
|
138
|
+
}),
|
|
139
|
+
updatePost: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
|
|
140
|
+
query: ({ id, title }) => ({
|
|
141
|
+
url: `posts/${id}`,
|
|
142
|
+
method: 'PATCH',
|
|
143
|
+
body: { title },
|
|
144
|
+
}),
|
|
145
|
+
invalidatesTags: (_result, _error, { id }) => [{ type: 'Post', id }],
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Treat tags as the normal invalidation path before reaching for manual cache patching.
|
|
152
|
+
|
|
153
|
+
### Do optimistic updates in endpoint lifecycles
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
type Post = { id: string; title: string }
|
|
157
|
+
|
|
158
|
+
export const api = createApi({
|
|
159
|
+
reducerPath: 'api',
|
|
160
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
161
|
+
tagTypes: ['Post'],
|
|
162
|
+
endpoints: (build) => ({
|
|
163
|
+
getPosts: build.query<Post[], void>({
|
|
164
|
+
query: () => 'posts',
|
|
165
|
+
providesTags: ['Post'],
|
|
166
|
+
}),
|
|
167
|
+
updatePostTitle: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
|
|
168
|
+
query: ({ id, title }) => ({
|
|
169
|
+
url: `posts/${id}`,
|
|
170
|
+
method: 'PATCH',
|
|
171
|
+
body: { title },
|
|
172
|
+
}),
|
|
173
|
+
async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) {
|
|
174
|
+
const patch = dispatch(
|
|
175
|
+
api.util.updateQueryData('getPosts', undefined, (draft) => {
|
|
176
|
+
const post = draft.find((item) => item.id === id)
|
|
177
|
+
if (post) {
|
|
178
|
+
post.title = title
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await queryFulfilled
|
|
185
|
+
} catch {
|
|
186
|
+
patch.undo()
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Keep optimistic and pessimistic cache updates inside endpoint lifecycle handlers so they stay coupled to the request.
|
|
195
|
+
|
|
196
|
+
## Common Mistakes
|
|
197
|
+
|
|
198
|
+
### CRITICAL Creating multiple API slices for one backend
|
|
199
|
+
|
|
200
|
+
Wrong:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
204
|
+
|
|
205
|
+
type User = { id: string; name: string }
|
|
206
|
+
|
|
207
|
+
const baseQuery = fetchBaseQuery({ baseUrl: '/api/' })
|
|
208
|
+
|
|
209
|
+
const postsApi = createApi({
|
|
210
|
+
reducerPath: 'api',
|
|
211
|
+
baseQuery,
|
|
212
|
+
endpoints: () => ({}),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const usersApi = createApi({
|
|
216
|
+
reducerPath: 'api',
|
|
217
|
+
baseQuery,
|
|
218
|
+
endpoints: () => ({}),
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Correct:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
226
|
+
|
|
227
|
+
type User = { id: string; name: string }
|
|
228
|
+
|
|
229
|
+
const baseQuery = fetchBaseQuery({ baseUrl: '/api/' })
|
|
230
|
+
|
|
231
|
+
const api = createApi({
|
|
232
|
+
reducerPath: 'api',
|
|
233
|
+
baseQuery,
|
|
234
|
+
endpoints: () => ({}),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const usersApi = api.injectEndpoints({
|
|
238
|
+
endpoints: (build) => ({
|
|
239
|
+
getUsers: build.query<User[], void>({ query: () => 'users' }),
|
|
240
|
+
}),
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
One API slice per base URL preserves invalidation behavior and avoids duplicated middleware work.
|
|
245
|
+
|
|
246
|
+
Source: reduxjs/redux-toolkit:docs/rtk-query/api/createApi.mdx
|
|
247
|
+
|
|
248
|
+
### HIGH Forgetting `api.reducer` or `api.middleware`
|
|
249
|
+
|
|
250
|
+
Wrong:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
254
|
+
|
|
255
|
+
const store = configureStore({
|
|
256
|
+
reducer: {},
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Correct:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
264
|
+
|
|
265
|
+
const store = configureStore({
|
|
266
|
+
reducer: {
|
|
267
|
+
[api.reducerPath]: api.reducer,
|
|
268
|
+
},
|
|
269
|
+
middleware: (getDefaultMiddleware) =>
|
|
270
|
+
getDefaultMiddleware().concat(api.middleware),
|
|
271
|
+
})
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
RTK Query hooks need both the reducer and middleware to manage cache state and request lifecycles.
|
|
275
|
+
|
|
276
|
+
Source: reduxjs/redux-toolkit:docs/tutorials/rtk-query.mdx
|
|
277
|
+
|
|
278
|
+
### MEDIUM Persisting browser API cache by default
|
|
279
|
+
|
|
280
|
+
Wrong:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const storage = window.localStorage
|
|
284
|
+
|
|
285
|
+
const persistConfig = {
|
|
286
|
+
key: 'root',
|
|
287
|
+
storage,
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Correct:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|
295
|
+
|
|
296
|
+
const api = createApi({
|
|
297
|
+
reducerPath: 'api',
|
|
298
|
+
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
|
|
299
|
+
endpoints: () => ({}),
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Persisting RTK Query cache in browsers often keeps stale data around longer than users expect; treat persistence as a special case, not the default.
|
|
304
|
+
|
|
305
|
+
Source: reduxjs/redux-toolkit:docs/rtk-query/usage/persistence-and-rehydration.mdx
|
|
306
|
+
|
|
307
|
+
### HIGH Patching cache from components
|
|
308
|
+
|
|
309
|
+
Wrong:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { useEffect } from 'react'
|
|
313
|
+
import { useAppDispatch } from '../../app/hooks'
|
|
314
|
+
|
|
315
|
+
const dispatch = useAppDispatch()
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
dispatch(api.util.updateQueryData('getPosts', undefined, (draft) => {
|
|
319
|
+
draft.push({ id: 'p3', title: 'Patched from component' })
|
|
320
|
+
}))
|
|
321
|
+
}, [dispatch])
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Correct:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
updatePostTitle: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
|
|
328
|
+
query: ({ id, title }) => ({
|
|
329
|
+
url: `posts/${id}`,
|
|
330
|
+
method: 'PATCH',
|
|
331
|
+
body: { title },
|
|
332
|
+
}),
|
|
333
|
+
async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) {
|
|
334
|
+
const patch = dispatch(
|
|
335
|
+
api.util.updateQueryData('getPosts', undefined, (draft) => {
|
|
336
|
+
const post = draft.find((item) => item.id === id)
|
|
337
|
+
if (post) {
|
|
338
|
+
post.title = title
|
|
339
|
+
}
|
|
340
|
+
}),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
await queryFulfilled
|
|
345
|
+
} catch {
|
|
346
|
+
patch.undo()
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Component-level cache patches drift away from the mutation lifecycle that should own them.
|
|
353
|
+
|
|
354
|
+
Source: reduxjs/redux-toolkit:docs/rtk-query/usage/manual-cache-updates.mdx
|
|
355
|
+
|
|
356
|
+
### HIGH Expecting invalidation to refetch unsubscribed queries
|
|
357
|
+
|
|
358
|
+
Wrong:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { api } from './api'
|
|
362
|
+
import { store } from './store'
|
|
363
|
+
|
|
364
|
+
const subscription = store.dispatch(api.endpoints.getPosts.initiate())
|
|
365
|
+
subscription.unsubscribe()
|
|
366
|
+
store.dispatch(api.util.invalidateTags(['Post']))
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Correct:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
import { api } from './api'
|
|
373
|
+
import { store } from './store'
|
|
374
|
+
|
|
375
|
+
store.dispatch(api.endpoints.getPosts.initiate())
|
|
376
|
+
store.dispatch(api.util.invalidateTags(['Post']))
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Invalidation only refetches actively subscribed queries; if no component is using that cache entry, RTK Query drops it and fetches again next time it is needed.
|
|
380
|
+
|
|
381
|
+
Source: reduxjs/redux-toolkit:docs/rtk-query/usage/automated-refetching.mdx
|
|
382
|
+
|
|
383
|
+
## References
|
|
384
|
+
|
|
385
|
+
- [Endpoint lifecycle details and cache tradeoffs](references/endpoint-lifecycle.md)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Endpoint Lifecycle
|
|
2
|
+
|
|
3
|
+
## Invalidation rule
|
|
4
|
+
|
|
5
|
+
When a mutation invalidates tags:
|
|
6
|
+
|
|
7
|
+
- active subscribers refetch
|
|
8
|
+
- inactive cache entries are removed
|
|
9
|
+
- removed entries fetch again only when something subscribes later
|
|
10
|
+
|
|
11
|
+
That behavior is deliberate; invalidation is not a background "refresh everything" switch.
|
|
12
|
+
|
|
13
|
+
## Document cache tradeoff
|
|
14
|
+
|
|
15
|
+
RTK Query is a document cache, not a normalized entity graph cache.
|
|
16
|
+
|
|
17
|
+
Use RTK Query by default when:
|
|
18
|
+
|
|
19
|
+
- the data comes from request/response APIs
|
|
20
|
+
- document caching is acceptable
|
|
21
|
+
- tag invalidation and endpoint lifecycles solve the problem
|
|
22
|
+
|
|
23
|
+
Reach for a different tool when:
|
|
24
|
+
|
|
25
|
+
- the real requirement is a normalized graph cache
|
|
26
|
+
- the stack already has a domain-specific normalized client that fits better
|
|
27
|
+
|
|
28
|
+
If normalized caching is mandatory and no better library is already in the stack, a slice plus thunk flow may be the fallback.
|
|
29
|
+
|
|
30
|
+
## Useful endpoint options
|
|
31
|
+
|
|
32
|
+
- `providesTags`: tell RTK Query what cache entries this query represents
|
|
33
|
+
- `invalidatesTags`: tell RTK Query what a mutation dirties
|
|
34
|
+
- `onQueryStarted`: optimistic and pessimistic updates tied to a request
|
|
35
|
+
- `onCacheEntryAdded`: long-lived subscriptions such as streaming data
|
|
36
|
+
- `keepUnusedDataFor`: how long inactive cache entries stay around
|