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 1 commit
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
Prev Previous commit
Next Next commit
Merge branch 'v2.0-integration' into pr/configure-produce-implementation
  • Loading branch information
ben.durrant committed May 16, 2023
commit 080974d0767413368eb5ed2b802bb3416a472816
315 changes: 263 additions & 52 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Reducer } from 'redux'
import type { Action, AnyAction, Reducer } from 'redux'
import type {
ActionCreatorWithoutPayload,
PayloadAction,
Expand All @@ -16,19 +16,17 @@ import type {
import { buildCreateReducer } from './createReducer'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer } from './tsHelpers'
import { immutableHelpers } from './immer'

let hasWarnedAboutObjectNotation = false

/**
* An action creator attached to a slice.
*
* @deprecated please use PayloadActionCreator directly
*
* @public
*/
export type SliceActionCreator<P> = PayloadActionCreator<P>
import type { Id, NoInfer, Tail } from './tsHelpers'
import type { CombinedSliceReducer, InjectConfig } from './combineSlices'
import type {
AsyncThunk,
AsyncThunkConfig,
AsyncThunkOptions,
AsyncThunkPayloadCreator,
OverrideThunkApiConfigs,
} from './createAsyncThunk'
import { createAsyncThunk } from './createAsyncThunk'

/**
* The return value of `createSlice`
Expand Down Expand Up @@ -547,10 +545,18 @@ export type CreateSlice = {
<
State,
CaseReducers extends SliceCaseReducers<State>,
Name extends string = string
Name extends string,
Selectors extends SliceSelectors<State>,
ReducerPath extends string = Name
>(
options: CreateSliceOptions<State, CaseReducers, Name>
): Slice<State, CaseReducers, Name>
options: CreateSliceOptions<
State,
CaseReducers,
Name,
ReducerPath,
Selectors
>
): Slice<State, CaseReducers, Name, ReducerPath, Selectors>
}

export interface BuildCreateSliceConfiguration
Expand All @@ -560,16 +566,23 @@ export function buildCreateSlice(
configuration: BuildCreateSliceConfiguration
): CreateSlice {
const createReducer = buildCreateReducer(configuration)
const { freeze } = configuration

return function createSlice<
State,
CaseReducers extends SliceCaseReducers<State>,
Name extends string = string
Name extends string,
Selectors extends SliceSelectors<State>,
ReducerPath extends string = Name
>(
options: CreateSliceOptions<State, CaseReducers, Name>
): Slice<State, CaseReducers, Name> {
const { name } = options
options: CreateSliceOptions<
State,
CaseReducers,
Name,
ReducerPath,
Selectors
>
): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
const { name, reducerPath = name as unknown as ReducerPath } = options
if (!name) {
throw new Error('`name` is a required option for createSlice')
}
Expand All @@ -584,39 +597,39 @@ export function buildCreateSlice(
)
}
}

const initialState =
typeof options.initialState == 'function'
? options.initialState
: freeze(options.initialState)

const reducers = options.reducers || {}
const reducers =
(typeof options.reducers === 'function'
? options.reducers(buildReducerCreators<State>())
: options.reducers) || {}

const reducerNames = Object.keys(reducers)

const sliceCaseReducersByName: Record<string, CaseReducer> = {}
const sliceCaseReducersByType: Record<string, CaseReducer> = {}
const actionCreators: Record<string, Function> = {}
const context: ReducerHandlingContext<State> = {
sliceCaseReducersByName: {},
sliceCaseReducersByType: {},
actionCreators: {},
}

reducerNames.forEach((reducerName) => {
const maybeReducerWithPrepare = reducers[reducerName]
const type = getType(name, reducerName)

let caseReducer: CaseReducer<State, any>
let prepareCallback: PrepareAction<any> | undefined

if ('reducer' in maybeReducerWithPrepare) {
caseReducer = maybeReducerWithPrepare.reducer
prepareCallback = maybeReducerWithPrepare.prepare
const reducerDefinition = reducers[reducerName]
const reducerDetails: ReducerDetails = {
reducerName,
type: getType(name, reducerName),
createNotation: typeof options.reducers === 'function',
}
if (isAsyncThunkSliceReducerDefinition<State>(reducerDefinition)) {
handleThunkCaseReducerDefinition(
reducerDetails,
reducerDefinition,
context
)
} else {
caseReducer = maybeReducerWithPrepare
handleNormalReducerDefinition<State>(
reducerDetails,
reducerDefinition,
context
)
}

sliceCaseReducersByName[reducerName] = caseReducer
sliceCaseReducersByType[type] = caseReducer
actionCreators[reducerName] = prepareCallback
? createAction(type, prepareCallback)
: createAction(type)
})

function buildReducer() {
Expand All @@ -636,9 +649,12 @@ export function buildCreateSlice(
? executeReducerBuilderCallback(options.extraReducers)
: [options.extraReducers]

const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
const finalCaseReducers = {
...extraReducers,
...context.sliceCaseReducersByType,
}

return createReducer(initialState, (builder) => {
return createReducer(options.initialState, (builder) => {
for (let key in finalCaseReducers) {
builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
}
Expand All @@ -651,24 +667,219 @@ export function buildCreateSlice(
})
}

const defaultSelectSlice = (
rootState: { [K in ReducerPath]: State }
): State => rootState[reducerPath]

const selectSelf = (state: State) => state

const injectedSelectorCache = new WeakMap<
Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
WeakMap<
(rootState: any) => State | undefined,
Record<string, (rootState: any) => any>
>
>()

let _reducer: ReducerWithInitialState<State>

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

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

return _reducer.getInitialState()
},
getSelectors(selectState: (rootState: any) => State = selectSelf) {
let selectorCache = injectedSelectorCache.get(this)
if (!selectorCache) {
selectorCache = new WeakMap()
injectedSelectorCache.set(this, selectorCache)
}
let cached = selectorCache.get(selectState)
if (!cached) {
cached = {}
for (const [name, selector] of Object.entries(
options.selectors ?? {}
)) {
cached[name] = (rootState: any, ...args: any[]) => {
let sliceState = selectState(rootState)
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(
'selectState returned undefined for an uninjected slice reducer'
)
}
}
return selector(sliceState, ...args)
}
}
selectorCache.set(selectState, cached)
}
return cached as any
},
get selectors() {
return this.getSelectors(defaultSelectSlice)
},
injectInto(injectable, { reducerPath, ...config } = {}) {
injectable.inject(
{
reducerPath: reducerPath ?? this.reducerPath,
reducer: this.reducer,
},
config
)
return {
...this,
get selectors() {
return this.getSelectors(defaultSelectSlice)
},
} as any
},
}
return slice
}
}

interface ReducerHandlingContext<State> {
sliceCaseReducersByName: Record<
string,
| CaseReducer<State, any>
| Pick<
AsyncThunkSliceReducerDefinition<State, any, any, any>,
'fulfilled' | 'rejected' | 'pending'
>
>
sliceCaseReducersByType: Record<string, CaseReducer<State, any>>
actionCreators: Record<string, Function>
}

interface ReducerDetails {
reducerName: string
type: string
createNotation: boolean
}

function buildReducerCreators<State>(): ReducerCreators<State> {
function asyncThunk(
payloadCreator: AsyncThunkPayloadCreator<any, any>,
config: AsyncThunkSliceReducerConfig<State, any>
): AsyncThunkSliceReducerDefinition<State, any> {
return {
[reducerDefinitionType]: ReducerType.asyncThunk,
payloadCreator,
...config,
}
}
asyncThunk.withTypes = () => asyncThunk
return {
reducer(caseReducer) {
return Object.assign(
{
// hack so the wrapping function has the same name as the original
// we need to create a wrapper so the `reducerDefinitionType` is not assigned to the original
[caseReducer.name](...args: Parameters<typeof caseReducer>) {
return caseReducer(...args)
},
}[caseReducer.name],
{
[reducerDefinitionType]: ReducerType.reducer,
} as const
)
},
preparedReducer(prepare, reducer) {
return {
[reducerDefinitionType]: ReducerType.reducerWithPrepare,
prepare,
reducer,
}
},
asyncThunk: asyncThunk as any,
}
}

function handleNormalReducerDefinition<State>(
{ type, reducerName, createNotation }: ReducerDetails,
maybeReducerWithPrepare:
| CaseReducer<State, { payload: any; type: string }>
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>,
context: ReducerHandlingContext<State>
) {
let caseReducer: CaseReducer<State, any>
let prepareCallback: PrepareAction<any> | undefined
if ('reducer' in maybeReducerWithPrepare) {
if (
createNotation &&
!isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare)
) {
throw new Error(
'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.'
)
}
caseReducer = maybeReducerWithPrepare.reducer
prepareCallback = maybeReducerWithPrepare.prepare
} else {
caseReducer = maybeReducerWithPrepare
}
context.sliceCaseReducersByName[reducerName] = caseReducer
context.sliceCaseReducersByType[type] = caseReducer
context.actionCreators[reducerName] = prepareCallback
? createAction(type, prepareCallback)
: createAction(type)
}

function isAsyncThunkSliceReducerDefinition<State>(
reducerDefinition: any
): reducerDefinition is AsyncThunkSliceReducerDefinition<State, any, any, any> {
return reducerDefinition[reducerDefinitionType] === ReducerType.asyncThunk
}

function isCaseReducerWithPrepareDefinition<State>(
reducerDefinition: any
): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> {
return (
reducerDefinition[reducerDefinitionType] === ReducerType.reducerWithPrepare
)
}

function handleThunkCaseReducerDefinition<State>(
{ type, reducerName }: ReducerDetails,
reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,
context: ReducerHandlingContext<State>
) {
const { payloadCreator, fulfilled, pending, rejected, options } =
reducerDefinition
const thunk = createAsyncThunk(type, payloadCreator, options as any)
context.actionCreators[reducerName] = thunk

if (fulfilled) {
context.sliceCaseReducersByType[thunk.fulfilled.type] = fulfilled
}
if (pending) {
context.sliceCaseReducersByType[thunk.pending.type] = pending
}
if (rejected) {
context.sliceCaseReducersByType[thunk.rejected.type] = rejected
}

context.sliceCaseReducersByName[reducerName] = {
fulfilled: fulfilled || noop,
pending: pending || noop,
rejected: rejected || noop,
}
}

function noop() {}

export const createSlice = buildCreateSlice(immutableHelpers)
3 changes: 3 additions & 0 deletions packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,6 @@ export type { AutoBatchOptions } from './autoBatchEnhancer'
export type { ImmutableHelpers } from './tsHelpers'
export { defineImmutableHelpers } from './tsHelpers'
export { immutableHelpers as immerImmutableHelpers } from './immer'

export { combineSlices } from './combineSlices'
export type { WithSlice } from './combineSlices'
Loading
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.