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

Check initial state is draftable before using immer to freeze it. #2378

Merged
merged 2 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions packages/toolkit/src/createReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AnyAction, Action, Reducer } from 'redux'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer } from './tsHelpers'
import { freezeDraftable } from './utils'

/**
* Defines a mapping from action types to corresponding action object shapes.
Expand Down Expand Up @@ -223,12 +224,12 @@ export function createReducer<S extends NotFunction<any>>(
? executeReducerBuilderCallback(mapOrBuilderCallback)
: [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]

// Ensure the initial state gets frozen either way
// Ensure the initial state gets frozen either way (if draftable)
let getInitialState: () => S
if (isStateFunction(initialState)) {
getInitialState = () => createNextState(initialState(), () => {})
getInitialState = () => freezeDraftable(initialState())
} else {
const frozenInitialState = createNextState(initialState, () => {})
const frozenInitialState = freezeDraftable(initialState)
getInitialState = () => frozenInitialState
}

Expand Down
20 changes: 10 additions & 10 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { createReducer, NotFunction } from './createReducer'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer } from './tsHelpers'
import { freezeDraftable } from './utils'

/**
* An action creator attached to a slice.
Expand Down Expand Up @@ -226,16 +227,15 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
export type ValidateSliceCaseReducers<
S,
ACR extends SliceCaseReducers<S>
> = ACR &
{
[T in keyof ACR]: ACR[T] extends {
reducer(s: S, action?: infer A): any
}
? {
prepare(...a: never[]): Omit<A, 'type'>
}
: {}
> = ACR & {
[T in keyof ACR]: ACR[T] extends {
reducer(s: S, action?: infer A): any
}
? {
prepare(...a: never[]): Omit<A, 'type'>
}
: {}
}

function getType(slice: string, actionKey: string): string {
return `${slice}/${actionKey}`
Expand Down Expand Up @@ -265,7 +265,7 @@ export function createSlice<
const initialState =
typeof options.initialState == 'function'
? options.initialState
: createNextState(options.initialState, () => {})
: freezeDraftable(options.initialState)

const reducers = options.reducers || {}

Expand Down
3 changes: 3 additions & 0 deletions packages/toolkit/src/tests/createReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ describe('createReducer', () => {
/Cannot assign to read only property/
)
})
test('does not throw error if initial state is not draftable', () => {
expect(() => createReducer(new URLSearchParams(), {})).not.toThrowError()
})
})

describe('given pure reducers with immutable updates', () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/toolkit/src/tests/createSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ describe('createSlice', () => {

expect(slice.getInitialState()).toBe(initialState)
})

it('should allow non-draftable initial state', () => {
expect(() =>
createSlice({
name: 'params',
initialState: new URLSearchParams(),
reducers: {},
})
).not.toThrowError()
})
})

describe('when initialState is a function', () => {
Expand Down Expand Up @@ -105,6 +115,16 @@ describe('createSlice', () => {

expect(slice.getInitialState()).toBe(42)
})

it('should allow non-draftable initial state', () => {
expect(() =>
createSlice({
name: 'params',
initialState: () => new URLSearchParams(),
reducers: {},
})
).not.toThrowError()
})
})

describe('when mutating state object', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/toolkit/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import createNextState, { isDraftable } from 'immer'
import type { Middleware } from 'redux'

export function getTimeMeasureUtils(maxDelay: number, fnName: string) {
Expand Down Expand Up @@ -64,3 +65,7 @@ export class MiddlewareArray<
return new MiddlewareArray(...arr.concat(this))
}
}

export function freezeDraftable<T>(val: T) {
return isDraftable(val) ? createNextState(val, () => {}) : val
}