Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/reduxjs/redux-toolkit int…
Browse files Browse the repository at this point in the history
…o rn-example-ci
  • Loading branch information
aryaemami59 committed Jan 23, 2024
2 parents 3e186d6 + 6e66f4f commit 480ba72
Show file tree
Hide file tree
Showing 26 changed files with 541 additions and 161 deletions.
2 changes: 1 addition & 1 deletion docs/api/createAsyncThunk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ If a thunk was cancelled, the result of the promise will be a `rejected` action
So if you wanted to test that a thunk was cancelled before executing, you can do the following:

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

test('this thunk should always be skipped', async () => {
const thunk = createAsyncThunk(
Expand Down
2 changes: 1 addition & 1 deletion docs/introduction/why-rtk-is-redux-today.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ See these pages to learn how to use "modern Redux" with Redux Toolkit:

:::

## How Redux Toolkit Is Different Than the Redux Core
## How Redux Toolkit Is Different From the Redux Core

### What Is "Redux"?

Expand Down
21 changes: 21 additions & 0 deletions docs/rtk-query/usage/customizing-create-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ const customCreateApi = buildCreateApi(
)
```

## Customizing `createSelector` for RTKQ

Both `coreModule` and `reactHooksModule` accept a `createSelector` option which should be a selector creator instance from Reselect or with an equivalent signature.

```ts
import * as React from 'react'
import { createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'

const createLruSelector = createSelectorCreator(lruMemoize)

const customCreateApi = buildCreateApi(
coreModule({ createSelector: createLruSelector }),
reactHooksModule({ createSelector: createLruSelector })
)
```

## Creating your own module

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const slice = createSlice({
console.log('fulfilled', action)
state.user = action.payload.user
state.token = action.payload.token
state.isAuthenticated = true
})
.addMatcher(postsApi.endpoints.login.matchRejected, (state, action) => {
console.log('rejected', action)
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"prettier": "^2.2.1",
"release-it": "^14.12.5",
"serve": "^14.2.0",
"typescript": "5.2"
"typescript": "^5.2.2"
},
"resolutions": {
"@babel/core": "7.19.3",
Expand Down Expand Up @@ -68,8 +68,7 @@
"type-fest": "2.19.0",
"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",
"@typescript-eslint/eslint-plugin": "6.12.0",
"@typescript-eslint/parser": "6.12.0",
"typescript": "5.2.2"
"@typescript-eslint/parser": "6.12.0"
},
"scripts": {
"build": "yarn build:packages",
Expand Down
4 changes: 2 additions & 2 deletions packages/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
}
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.13.1",
"@arethetypeswrong/cli": "^0.13.5",
"@microsoft/api-extractor": "^7.13.2",
"@phryneas/ts-version": "^1.0.2",
"@size-limit/preset-small-lib": "^4.11.0",
Expand Down Expand Up @@ -100,7 +100,7 @@
"format": "prettier --write \"(src|examples)/**/*.{ts,tsx}\" \"**/*.md\"",
"format:check": "prettier --list-different \"(src|examples)/**/*.{ts,tsx}\" \"docs/*/**.md\"",
"lint": "eslint src examples",
"test": "vitest",
"test": "vitest --run",
"type-tests": "yarn tsc -p src/tests/tsconfig.typetests.json && yarn tsc -p src/query/tests/tsconfig.typetests.json",
"prepack": "yarn build"
},
Expand Down
19 changes: 13 additions & 6 deletions packages/toolkit/src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
Id,
IsAny,
IsUnknown,
SafePromise,
TypeGuard,
} from './tsHelpers'
import { nanoid } from './nanoid'
Expand Down Expand Up @@ -242,7 +243,7 @@ export type AsyncThunkAction<
dispatch: GetDispatch<ThunkApiConfig>,
getState: () => GetState<ThunkApiConfig>,
extra: GetExtra<ThunkApiConfig>
) => Promise<
) => SafePromise<
| ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>>
| ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>
> & {
Expand Down Expand Up @@ -577,6 +578,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
: nanoid()

const abortController = new AbortController()
let abortHandler: (() => void) | undefined
let abortReason: string | undefined

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

const abortedPromise = new Promise<never>((_, reject) =>
abortController.signal.addEventListener('abort', () =>
const abortedPromise = new Promise<never>((_, reject) => {
abortHandler = () => {
reject({
name: 'AbortError',
message: abortReason || 'Aborted',
})
)
)
}
abortController.signal.addEventListener('abort', abortHandler)
})
dispatch(
pending(
requestId,
Expand Down Expand Up @@ -653,6 +656,10 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
err instanceof RejectWithValue
? rejected(null, requestId, arg, err.payload, err.meta)
: rejected(err as any, requestId, arg)
} finally {
if (abortHandler) {
abortController.signal.removeEventListener('abort', abortHandler)
}
}
// We dispatch the result action _after_ the catch, to avoid having any errors
// here get swallowed by the try/catch block,
Expand All @@ -670,7 +677,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
}
return finalAction
})()
return Object.assign(promise as Promise<any>, {
return Object.assign(promise as SafePromise<any>, {
abort,
requestId,
arg,
Expand Down
18 changes: 11 additions & 7 deletions packages/toolkit/src/createDraftSafeSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ export const createDraftSafeSelectorCreator: typeof createSelectorCreator = (
...args: unknown[]
) => {
const createSelector = (createSelectorCreator as any)(...args)
return (...args: unknown[]) => {
const selector = createSelector(...args)
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
selector(isDraft(value) ? current(value) : value, ...rest)
Object.assign(wrappedSelector, selector)
return wrappedSelector as any
}
const createDraftSafeSelector = Object.assign(
(...args: unknown[]) => {
const selector = createSelector(...args)
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
selector(isDraft(value) ? current(value) : value, ...rest)
Object.assign(wrappedSelector, selector)
return wrappedSelector as any
},
{ withTypes: () => createDraftSafeSelector }
)
return createDraftSafeSelector
}

/**
Expand Down
120 changes: 69 additions & 51 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,21 @@ export interface Slice<
/**
* Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
*/
getSelectors(this: this): Id<SliceDefinedSelectors<State, Selectors, State>>
getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>>

/**
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
*/
getSelectors<RootState>(
this: this,
selectState: (this: this, rootState: RootState) => State
selectState: (rootState: RootState) => State
): Id<SliceDefinedSelectors<State, Selectors, RootState>>

/**
* Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case)
*
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`.
*/
selectors: Id<
get selectors(): Id<
SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
>

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

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

/**
* Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case)
*
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`.
*/
selectors: Id<
get selectors(): Id<
SliceDefinedSelectors<
State,
Selectors,
Expand Down Expand Up @@ -740,8 +739,8 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {

const selectSelf = (state: State) => state

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

let _reducer: ReducerWithInitialState<State>

const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
name,
reducerPath,
reducer(state, action) {
if (!_reducer) _reducer = buildReducer()
function reducer(state: State | undefined, action: UnknownAction) {
if (!_reducer) _reducer = buildReducer()

return _reducer(state, action)
},
actions: context.actionCreators as any,
caseReducers: context.sliceCaseReducersByName as any,
getInitialState() {
if (!_reducer) _reducer = buildReducer()
return _reducer(state, action)
}

return _reducer.getInitialState()
},
getSelectors(selectState: (rootState: any) => State = selectSelf) {
const selectorCache = emplace(injectedSelectorCache, this, {
function getInitialState() {
if (!_reducer) _reducer = buildReducer()

return _reducer.getInitialState()
}

function makeSelectorProps<CurrentReducerPath extends string = ReducerPath>(
reducerPath: CurrentReducerPath,
injected = false
): Pick<
Slice<State, CaseReducers, Name, CurrentReducerPath, Selectors>,
'getSelectors' | 'selectors' | 'selectSlice' | 'reducerPath'
> {
function selectSlice(state: { [K in CurrentReducerPath]: State }) {
let sliceState = state[reducerPath]
if (typeof sliceState === 'undefined') {
if (injected) {
sliceState = getInitialState()
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(
'selectSlice returned undefined for an uninjected slice reducer'
)
}
}
return sliceState
}
function getSelectors(
selectState: (rootState: any) => State = selectSelf
) {
const selectorCache = emplace(injectedSelectorCache, injected, {
insert: () => new WeakMap(),
})

Expand All @@ -777,39 +795,39 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
options.selectors ?? {}
)) {
map[name] = wrapSelector(
this,
selector,
selectState,
this !== slice
getInitialState,
injected
)
}
return map
},
}) as any
},
selectSlice(state) {
let sliceState = state[this.reducerPath]
if (typeof sliceState === 'undefined') {
// check if injectInto has been called
if (this !== slice) {
sliceState = this.getInitialState()
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(
'selectSlice returned undefined for an uninjected slice reducer'
)
}
}
return sliceState
},
get selectors() {
return this.getSelectors(this.selectSlice)
},
}
return {
reducerPath,
getSelectors,
get selectors() {
return getSelectors(selectSlice)
},
selectSlice,
}
}

const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
name,
reducer,
actions: context.actionCreators as any,
caseReducers: context.sliceCaseReducersByName as any,
getInitialState,
...makeSelectorProps(reducerPath),
injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) {
const reducerPath = pathOpt ?? this.reducerPath
injectable.inject({ reducerPath, reducer: this.reducer }, config)
const newReducerPath = pathOpt ?? reducerPath
injectable.inject({ reducerPath: newReducerPath, reducer }, config)
return {
...this,
reducerPath,
...slice,
...makeSelectorProps(newReducerPath, true),
} as any
},
}
Expand All @@ -818,16 +836,16 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
}

function wrapSelector<State, NewState, S extends Selector<State>>(
slice: Slice,
selector: S,
selectState: Selector<NewState, State>,
getInitialState: () => State,
injected?: boolean
) {
function wrapper(rootState: NewState, ...args: any[]) {
let sliceState = selectState.call(slice, rootState)
let sliceState = selectState(rootState)
if (typeof sliceState === 'undefined') {
if (injected) {
sliceState = slice.getInitialState()
sliceState = getInitialState()
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(
'selectState returned undefined for an uninjected slice reducer'
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,6 @@ export { combineSlices } from './combineSlices'

export type { WithSlice } from './combineSlices'

export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions } from './tsHelpers'
export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions, SafePromise } from './tsHelpers'

export { formatProdErrorMessage } from './formatProdErrorMessage'
Loading

0 comments on commit 480ba72

Please sign in to comment.