Skip to content

Commit 480ba72

Browse files
committed
Merge branch 'master' of https://github.com/reduxjs/redux-toolkit into rn-example-ci
2 parents 3e186d6 + 6e66f4f commit 480ba72

26 files changed

+541
-161
lines changed

docs/api/createAsyncThunk.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ If a thunk was cancelled, the result of the promise will be a `rejected` action
527527
So if you wanted to test that a thunk was cancelled before executing, you can do the following:
528528

529529
```ts no-transpile
530-
import { createAsyncThunk, isRejected } from '@reduxjs/toolkit'
530+
import { createAsyncThunk } from '@reduxjs/toolkit'
531531

532532
test('this thunk should always be skipped', async () => {
533533
const thunk = createAsyncThunk(

docs/introduction/why-rtk-is-redux-today.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ See these pages to learn how to use "modern Redux" with Redux Toolkit:
2929

3030
:::
3131

32-
## How Redux Toolkit Is Different Than the Redux Core
32+
## How Redux Toolkit Is Different From the Redux Core
3333

3434
### What Is "Redux"?
3535

docs/rtk-query/usage/customizing-create-api.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,27 @@ const customCreateApi = buildCreateApi(
4848
)
4949
```
5050

51+
## Customizing `createSelector` for RTKQ
52+
53+
Both `coreModule` and `reactHooksModule` accept a `createSelector` option which should be a selector creator instance from Reselect or with an equivalent signature.
54+
55+
```ts
56+
import * as React from 'react'
57+
import { createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'
58+
import {
59+
buildCreateApi,
60+
coreModule,
61+
reactHooksModule,
62+
} from '@reduxjs/toolkit/query/react'
63+
64+
const createLruSelector = createSelectorCreator(lruMemoize)
65+
66+
const customCreateApi = buildCreateApi(
67+
coreModule({ createSelector: createLruSelector }),
68+
reactHooksModule({ createSelector: createLruSelector })
69+
)
70+
```
71+
5172
## Creating your own module
5273

5374
If you want to create your own module, you should review [the react-hooks module](https://github.com/reduxjs/redux-toolkit/blob/b74a52935a5840bebca5acdc8e2265e3b6497afa/src/query/react/module.ts) to see what an implementation would look like.

examples/query/react/kitchen-sink/src/features/auth/authSlice.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const slice = createSlice({
2424
console.log('fulfilled', action)
2525
state.user = action.payload.user
2626
state.token = action.payload.token
27+
state.isAuthenticated = true
2728
})
2829
.addMatcher(postsApi.endpoints.login.matchRejected, (state, action) => {
2930
console.log('rejected', action)

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"prettier": "^2.2.1",
3737
"release-it": "^14.12.5",
3838
"serve": "^14.2.0",
39-
"typescript": "5.2"
39+
"typescript": "^5.2.2"
4040
},
4141
"resolutions": {
4242
"@babel/core": "7.19.3",
@@ -68,8 +68,7 @@
6868
"type-fest": "2.19.0",
6969
"console-testing-library@0.6.1": "patch:console-testing-library@npm%3A0.6.1#./.yarn/patches/console-testing-library-npm-0.6.1-4d9957d402.patch",
7070
"@typescript-eslint/eslint-plugin": "6.12.0",
71-
"@typescript-eslint/parser": "6.12.0",
72-
"typescript": "5.2.2"
71+
"@typescript-eslint/parser": "6.12.0"
7372
},
7473
"scripts": {
7574
"build": "yarn build:packages",

packages/toolkit/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
}
5050
},
5151
"devDependencies": {
52-
"@arethetypeswrong/cli": "^0.13.1",
52+
"@arethetypeswrong/cli": "^0.13.5",
5353
"@microsoft/api-extractor": "^7.13.2",
5454
"@phryneas/ts-version": "^1.0.2",
5555
"@size-limit/preset-small-lib": "^4.11.0",
@@ -100,7 +100,7 @@
100100
"format": "prettier --write \"(src|examples)/**/*.{ts,tsx}\" \"**/*.md\"",
101101
"format:check": "prettier --list-different \"(src|examples)/**/*.{ts,tsx}\" \"docs/*/**.md\"",
102102
"lint": "eslint src examples",
103-
"test": "vitest",
103+
"test": "vitest --run",
104104
"type-tests": "yarn tsc -p src/tests/tsconfig.typetests.json && yarn tsc -p src/query/tests/tsconfig.typetests.json",
105105
"prepack": "yarn build"
106106
},

packages/toolkit/src/createAsyncThunk.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
Id,
1212
IsAny,
1313
IsUnknown,
14+
SafePromise,
1415
TypeGuard,
1516
} from './tsHelpers'
1617
import { nanoid } from './nanoid'
@@ -242,7 +243,7 @@ export type AsyncThunkAction<
242243
dispatch: GetDispatch<ThunkApiConfig>,
243244
getState: () => GetState<ThunkApiConfig>,
244245
extra: GetExtra<ThunkApiConfig>
245-
) => Promise<
246+
) => SafePromise<
246247
| ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>>
247248
| ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>
248249
> & {
@@ -577,6 +578,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
577578
: nanoid()
578579

579580
const abortController = new AbortController()
581+
let abortHandler: (() => void) | undefined
580582
let abortReason: string | undefined
581583

582584
function abort(reason?: string) {
@@ -600,14 +602,15 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
600602
}
601603
}
602604

603-
const abortedPromise = new Promise<never>((_, reject) =>
604-
abortController.signal.addEventListener('abort', () =>
605+
const abortedPromise = new Promise<never>((_, reject) => {
606+
abortHandler = () => {
605607
reject({
606608
name: 'AbortError',
607609
message: abortReason || 'Aborted',
608610
})
609-
)
610-
)
611+
}
612+
abortController.signal.addEventListener('abort', abortHandler)
613+
})
611614
dispatch(
612615
pending(
613616
requestId,
@@ -653,6 +656,10 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
653656
err instanceof RejectWithValue
654657
? rejected(null, requestId, arg, err.payload, err.meta)
655658
: rejected(err as any, requestId, arg)
659+
} finally {
660+
if (abortHandler) {
661+
abortController.signal.removeEventListener('abort', abortHandler)
662+
}
656663
}
657664
// We dispatch the result action _after_ the catch, to avoid having any errors
658665
// here get swallowed by the try/catch block,
@@ -670,7 +677,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
670677
}
671678
return finalAction
672679
})()
673-
return Object.assign(promise as Promise<any>, {
680+
return Object.assign(promise as SafePromise<any>, {
674681
abort,
675682
requestId,
676683
arg,

packages/toolkit/src/createDraftSafeSelector.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ export const createDraftSafeSelectorCreator: typeof createSelectorCreator = (
55
...args: unknown[]
66
) => {
77
const createSelector = (createSelectorCreator as any)(...args)
8-
return (...args: unknown[]) => {
9-
const selector = createSelector(...args)
10-
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
11-
selector(isDraft(value) ? current(value) : value, ...rest)
12-
Object.assign(wrappedSelector, selector)
13-
return wrappedSelector as any
14-
}
8+
const createDraftSafeSelector = Object.assign(
9+
(...args: unknown[]) => {
10+
const selector = createSelector(...args)
11+
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
12+
selector(isDraft(value) ? current(value) : value, ...rest)
13+
Object.assign(wrappedSelector, selector)
14+
return wrappedSelector as any
15+
},
16+
{ withTypes: () => createDraftSafeSelector }
17+
)
18+
return createDraftSafeSelector
1519
}
1620

1721
/**

packages/toolkit/src/createSlice.ts

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -88,22 +88,21 @@ export interface Slice<
8888
/**
8989
* Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
9090
*/
91-
getSelectors(this: this): Id<SliceDefinedSelectors<State, Selectors, State>>
91+
getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>>
9292

9393
/**
9494
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
9595
*/
9696
getSelectors<RootState>(
97-
this: this,
98-
selectState: (this: this, rootState: RootState) => State
97+
selectState: (rootState: RootState) => State
9998
): Id<SliceDefinedSelectors<State, Selectors, RootState>>
10099

101100
/**
102101
* Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case)
103102
*
104103
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`.
105104
*/
106-
selectors: Id<
105+
get selectors(): Id<
107106
SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
108107
>
109108

@@ -126,7 +125,7 @@ export interface Slice<
126125
*
127126
* Will throw an error if slice is not found.
128127
*/
129-
selectSlice(this: this, state: { [K in ReducerPath]: State }): State
128+
selectSlice(state: { [K in ReducerPath]: State }): State
130129
}
131130

132131
/**
@@ -153,15 +152,15 @@ interface InjectedSlice<
153152
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
154153
*/
155154
getSelectors<RootState>(
156-
selectState: (this: this, rootState: RootState) => State | undefined
155+
selectState: (rootState: RootState) => State | undefined
157156
): Id<SliceDefinedSelectors<State, Selectors, RootState>>
158157

159158
/**
160159
* Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case)
161160
*
162161
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`.
163162
*/
164-
selectors: Id<
163+
get selectors(): Id<
165164
SliceDefinedSelectors<
166165
State,
167166
Selectors,
@@ -740,8 +739,8 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
740739

741740
const selectSelf = (state: State) => state
742741

743-
const injectedSelectorCache = new WeakMap<
744-
Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
742+
const injectedSelectorCache = new Map<
743+
boolean,
745744
WeakMap<
746745
(rootState: any) => State | undefined,
747746
Record<string, (rootState: any) => any>
@@ -750,23 +749,42 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
750749

751750
let _reducer: ReducerWithInitialState<State>
752751

753-
const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
754-
name,
755-
reducerPath,
756-
reducer(state, action) {
757-
if (!_reducer) _reducer = buildReducer()
752+
function reducer(state: State | undefined, action: UnknownAction) {
753+
if (!_reducer) _reducer = buildReducer()
758754

759-
return _reducer(state, action)
760-
},
761-
actions: context.actionCreators as any,
762-
caseReducers: context.sliceCaseReducersByName as any,
763-
getInitialState() {
764-
if (!_reducer) _reducer = buildReducer()
755+
return _reducer(state, action)
756+
}
765757

766-
return _reducer.getInitialState()
767-
},
768-
getSelectors(selectState: (rootState: any) => State = selectSelf) {
769-
const selectorCache = emplace(injectedSelectorCache, this, {
758+
function getInitialState() {
759+
if (!_reducer) _reducer = buildReducer()
760+
761+
return _reducer.getInitialState()
762+
}
763+
764+
function makeSelectorProps<CurrentReducerPath extends string = ReducerPath>(
765+
reducerPath: CurrentReducerPath,
766+
injected = false
767+
): Pick<
768+
Slice<State, CaseReducers, Name, CurrentReducerPath, Selectors>,
769+
'getSelectors' | 'selectors' | 'selectSlice' | 'reducerPath'
770+
> {
771+
function selectSlice(state: { [K in CurrentReducerPath]: State }) {
772+
let sliceState = state[reducerPath]
773+
if (typeof sliceState === 'undefined') {
774+
if (injected) {
775+
sliceState = getInitialState()
776+
} else if (process.env.NODE_ENV !== 'production') {
777+
throw new Error(
778+
'selectSlice returned undefined for an uninjected slice reducer'
779+
)
780+
}
781+
}
782+
return sliceState
783+
}
784+
function getSelectors(
785+
selectState: (rootState: any) => State = selectSelf
786+
) {
787+
const selectorCache = emplace(injectedSelectorCache, injected, {
770788
insert: () => new WeakMap(),
771789
})
772790

@@ -777,39 +795,39 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
777795
options.selectors ?? {}
778796
)) {
779797
map[name] = wrapSelector(
780-
this,
781798
selector,
782799
selectState,
783-
this !== slice
800+
getInitialState,
801+
injected
784802
)
785803
}
786804
return map
787805
},
788806
}) as any
789-
},
790-
selectSlice(state) {
791-
let sliceState = state[this.reducerPath]
792-
if (typeof sliceState === 'undefined') {
793-
// check if injectInto has been called
794-
if (this !== slice) {
795-
sliceState = this.getInitialState()
796-
} else if (process.env.NODE_ENV !== 'production') {
797-
throw new Error(
798-
'selectSlice returned undefined for an uninjected slice reducer'
799-
)
800-
}
801-
}
802-
return sliceState
803-
},
804-
get selectors() {
805-
return this.getSelectors(this.selectSlice)
806-
},
807+
}
808+
return {
809+
reducerPath,
810+
getSelectors,
811+
get selectors() {
812+
return getSelectors(selectSlice)
813+
},
814+
selectSlice,
815+
}
816+
}
817+
818+
const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
819+
name,
820+
reducer,
821+
actions: context.actionCreators as any,
822+
caseReducers: context.sliceCaseReducersByName as any,
823+
getInitialState,
824+
...makeSelectorProps(reducerPath),
807825
injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) {
808-
const reducerPath = pathOpt ?? this.reducerPath
809-
injectable.inject({ reducerPath, reducer: this.reducer }, config)
826+
const newReducerPath = pathOpt ?? reducerPath
827+
injectable.inject({ reducerPath: newReducerPath, reducer }, config)
810828
return {
811-
...this,
812-
reducerPath,
829+
...slice,
830+
...makeSelectorProps(newReducerPath, true),
813831
} as any
814832
},
815833
}
@@ -818,16 +836,16 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
818836
}
819837

820838
function wrapSelector<State, NewState, S extends Selector<State>>(
821-
slice: Slice,
822839
selector: S,
823840
selectState: Selector<NewState, State>,
841+
getInitialState: () => State,
824842
injected?: boolean
825843
) {
826844
function wrapper(rootState: NewState, ...args: any[]) {
827-
let sliceState = selectState.call(slice, rootState)
845+
let sliceState = selectState(rootState)
828846
if (typeof sliceState === 'undefined') {
829847
if (injected) {
830-
sliceState = slice.getInitialState()
848+
sliceState = getInitialState()
831849
} else if (process.env.NODE_ENV !== 'production') {
832850
throw new Error(
833851
'selectState returned undefined for an uninjected slice reducer'

packages/toolkit/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,6 @@ export { combineSlices } from './combineSlices'
203203

204204
export type { WithSlice } from './combineSlices'
205205

206-
export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions } from './tsHelpers'
206+
export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions, SafePromise } from './tsHelpers'
207207

208208
export { formatProdErrorMessage } from './formatProdErrorMessage'

0 commit comments

Comments
 (0)