Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: configurable produce implementation #3074

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
49f808a
allow `produce` to be swapped out in createReducer/createSlice
phryneas Jan 11, 2023
f0228f9
Make more Immer utils configurable
markerikson Feb 21, 2023
fb9d324
add buildable createDraftSafeSelector
Apr 4, 2023
293a9b9
make configurable createEntityAdapter
Apr 4, 2023
83a8931
unused type
Apr 4, 2023
411568a
declare immutable helper types once
Apr 4, 2023
d9e1341
build createDraftSafeSelector outside of createSelectorsFactory
Apr 4, 2023
e5f48f1
allow replacing RTKQ's usage of immer
Apr 5, 2023
4f6c897
use immutablehelpers for RTKQ's createSlice
Apr 5, 2023
28a87c9
named export instead of default
Apr 5, 2023
180d198
use specific freeze function instead of abusing createNextState
Apr 5, 2023
5bd963c
export buildCreateEntityAdapter
Apr 5, 2023
bc924e7
Merge branch 'v2.0-integration' into more-produce
Apr 5, 2023
9f9dfff
export ImmutableHelpers
Apr 5, 2023
a36f092
Merge branch 'v2.0-integration' into pr/configure-produce-implementation
Apr 5, 2023
27bcf3b
Merge branch 'pr/configure-produce-implementation' into more-produce
Apr 5, 2023
19d5b8d
create a defineImmutableHelpers identity function, and use for immer
Apr 5, 2023
9614298
don't use @internal import
Apr 5, 2023
7eb1fa0
import ImmutableHelpers type from RTK
Apr 5, 2023
115b856
fix entity adapter options not being optional
Apr 18, 2023
bfa1419
hope the PR gets happier
May 5, 2023
9d8f344
be extra assertive about nicking reselect's types
May 5, 2023
df861fd
Merge pull request #3327 from EskiMojo14/more-produce
markerikson May 5, 2023
080974d
Merge branch 'v2.0-integration' into pr/configure-produce-implementation
May 16, 2023
30b1b49
Merge branch 'v2.0-integration' into pr/configure-produce-implementation
EskiMojo14 May 29, 2023
7276087
Merge branch 'v2.0-integration' into pr/configure-produce-implementation
Oct 2, 2023
b43f501
rtkImports
Oct 2, 2023
0888fde
add missing createEntityAdapter overload
Oct 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions packages/toolkit/src/createDraftSafeSelector.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { current, isDraft } from 'immer'
import { createSelectorCreator, defaultMemoize } from 'reselect'
import type { ImmutableHelpers } from './tsHelpers'
import { immutableHelpers } from './immer'

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)
return wrappedSelector as any
export type BuildCreateDraftSafeSelectorConfiguration = Pick<
ImmutableHelpers,
'isDraft' | 'current'
>

export function buildCreateDraftSafeSelectorCreator({
isDraft,
current,
}: BuildCreateDraftSafeSelectorConfiguration): typeof createSelectorCreator {
return function createDraftSafeSelectorCreator(...args: unknown[]) {
const createSelector = (createSelectorCreator as any)(...args)
return function createDraftSafeSelector(...args: unknown[]) {
const selector = (createSelector as any)(...args)
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
selector(isDraft(value) ? current(value) : value, ...rest)
return wrappedSelector as any
}
}
}

export const createDraftSafeSelectorCreator =
buildCreateDraftSafeSelectorCreator(immutableHelpers)

/**
* "Draft-Safe" version of `reselect`'s `createSelector`:
* If an `immer`-drafted object is passed into the resulting selector's first argument,
Expand Down
164 changes: 92 additions & 72 deletions packages/toolkit/src/createReducer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { Draft } from 'immer'
import { produce as createNextState, isDraft, isDraftable } from 'immer'
import type { Action, Reducer, UnknownAction } from 'redux'
import type { UnknownAction, Action, Reducer } from 'redux'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer, TypeGuard } from './tsHelpers'
import { freezeDraftable } from './utils'
import type { ImmutableHelpers, NoInfer, TypeGuard } from './tsHelpers'
import { immutableHelpers } from './immer'

/**
* Defines a mapping from action types to corresponding action object shapes.
Expand Down Expand Up @@ -73,7 +72,8 @@ export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
getInitialState: () => S
}

/**
export type CreateReducer = {
/**
* A utility function that allows defining a reducer as a mapping from action
* type to *case reducer* functions that handle these action types. The
* reducer's initial state is passed as the first argument.
Expand Down Expand Up @@ -137,85 +137,105 @@ const reducer = createReducer(
```
* @public
*/
export function createReducer<S extends NotFunction<any>>(
initialState: S | (() => S),
mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S> {
if (process.env.NODE_ENV !== 'production') {
if (typeof mapOrBuilderCallback === 'object') {
throw new Error(
"The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
)
}
}
<S extends NotFunction<any>>(
initialState: S | (() => S),
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S>
}

export type BuildCreateReducerConfiguration = Pick<
ImmutableHelpers,
'createNextState' | 'isDraft' | 'isDraftable' | 'freeze'
>

let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
executeReducerBuilderCallback(mapOrBuilderCallback)
export function buildCreateReducer({
createNextState,
isDraft,
isDraftable,
freeze,
}: BuildCreateReducerConfiguration): CreateReducer {
return function createReducer<S extends NotFunction<any>>(
initialState: S | (() => S),
mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S> {
if (process.env.NODE_ENV !== 'production') {
if (typeof mapOrBuilderCallback === 'object') {
throw new Error(
"The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
)
}
}

// Ensure the initial state gets frozen either way (if draftable)
let getInitialState: () => S
if (isStateFunction(initialState)) {
getInitialState = () => freezeDraftable(initialState())
} else {
const frozenInitialState = freezeDraftable(initialState)
getInitialState = () => frozenInitialState
}
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
executeReducerBuilderCallback(mapOrBuilderCallback)

function reducer(state = getInitialState(), action: any): S {
let caseReducers = [
actionsMap[action.type],
...finalActionMatchers
.filter(({ matcher }) => matcher(action))
.map(({ reducer }) => reducer),
]
if (caseReducers.filter((cr) => !!cr).length === 0) {
caseReducers = [finalDefaultCaseReducer]
// Ensure the initial state gets frozen either way (if draftable)
let getInitialState: () => S
if (isStateFunction(initialState)) {
getInitialState = () => freeze(initialState(), true)
} else {
const frozenInitialState = freeze(initialState, true)
getInitialState = () => frozenInitialState
}

return caseReducers.reduce((previousState, caseReducer): S => {
if (caseReducer) {
if (isDraft(previousState)) {
// If it's already a draft, we must already be inside a `createNextState` call,
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
// inside an existing draft. It's safe to just pass the draft to the mutator.
const draft = previousState as Draft<S> // We can assume this is already a draft
const result = caseReducer(draft, action)

if (result === undefined) {
return previousState
}
function reducer(state = getInitialState(), action: any): S {
let caseReducers = [
actionsMap[action.type],
...finalActionMatchers
.filter(({ matcher }) => matcher(action))
.map(({ reducer }) => reducer),
]
if (caseReducers.filter((cr) => !!cr).length === 0) {
caseReducers = [finalDefaultCaseReducer]
}

return result as S
} else if (!isDraftable(previousState)) {
// If state is not draftable (ex: a primitive, such as 0), we want to directly
// return the caseReducer func and not wrap it with produce.
const result = caseReducer(previousState as any, action)
return caseReducers.reduce((previousState, caseReducer): S => {
if (caseReducer) {
if (isDraft(previousState)) {
EskiMojo14 marked this conversation as resolved.
Show resolved Hide resolved
// If it's already a draft, we must already be inside a `createNextState` call,
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
// inside an existing draft. It's safe to just pass the draft to the mutator.
const draft = previousState as Draft<S> // We can assume this is already a draft
const result = caseReducer(draft, action)

if (result === undefined) {
if (previousState === null) {
if (result === undefined) {
return previousState
}
throw Error(
'A case reducer on a non-draftable value must not return undefined'
)
}

return result as S
} else {
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
// these two types.
return createNextState(previousState, (draft: Draft<S>) => {
return caseReducer(draft, action)
})
return result as S
} else if (!isDraftable(previousState)) {
// If state is not draftable (ex: a primitive, such as 0), we want to directly
// return the caseReducer func and not wrap it with produce.
const result = caseReducer(previousState as any, action)

if (result === undefined) {
if (previousState === null) {
return previousState
}
throw Error(
'A case reducer on a non-draftable value must not return undefined'
)
}

return result as S
} else {
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
// these two types.
return createNextState(previousState, (draft: Draft<S>) => {
return caseReducer(draft, action)
})
}
}
}

return previousState
}, state)
}
return previousState
}, state)
}

reducer.getInitialState = getInitialState
reducer.getInitialState = getInitialState

return reducer as ReducerWithInitialState<S>
return reducer as ReducerWithInitialState<S>
}
}

export const createReducer = buildCreateReducer(immutableHelpers)
Loading
Loading