Skip to content

Commit

Permalink
Add one-time runtime deprecation warnings for reducer object notation
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Aug 14, 2022
1 parent aa7dafc commit a254fb5
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 18 deletions.
13 changes: 13 additions & 0 deletions packages/toolkit/src/createReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
getInitialState: () => S
}

let hasWarnedAboutObjectNotation = false

/**
* A utility function that allows defining a reducer as a mapping from action
* type to *case reducer* functions that handle these action types. The
Expand Down Expand Up @@ -219,6 +221,17 @@ export function createReducer<S extends NotFunction<any>>(
actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
defaultCaseReducer?: CaseReducer<S>
): ReducerWithInitialState<S> {
if (process.env.NODE_ENV !== 'production') {
if (typeof mapOrBuilderCallback === 'object') {
if (!hasWarnedAboutObjectNotation) {
hasWarnedAboutObjectNotation = true
console.warn(
"The object notation for `createReducer` is deprecated, and will be removed in RTK 2.0. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
)
}
}
}

let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
typeof mapOrBuilderCallback === 'function'
? executeReducerBuilderCallback(mapOrBuilderCallback)
Expand Down
53 changes: 37 additions & 16 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer } from './tsHelpers'
import { freezeDraftable } from './utils'

let hasWarnedAboutObjectNotation = false

/**
* An action creator attached to a slice.
*
Expand Down Expand Up @@ -243,15 +245,16 @@ 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
> = ACR &
{
[T in keyof ACR]: ACR[T] extends {
reducer(s: S, action?: infer A): any
}
? {
prepare(...a: never[]): Omit<A, 'type'>
}
: {}
}
? {
prepare(...a: never[]): Omit<A, 'type'>
}
: {}
}

function getType(slice: string, actionKey: string): string {
return `${slice}/${actionKey}`
Expand Down Expand Up @@ -283,8 +286,10 @@ export function createSlice<
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
if(options.initialState === undefined) {
console.error('You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`')
if (options.initialState === undefined) {
console.error(
'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`'
)
}
}

Expand Down Expand Up @@ -323,6 +328,16 @@ export function createSlice<
})

function buildReducer() {
if (process.env.NODE_ENV !== 'production') {
if (typeof options.extraReducers === 'object') {
if (!hasWarnedAboutObjectNotation) {
hasWarnedAboutObjectNotation = true
console.warn(
"The object notation for `createSlice.extraReducers` is deprecated, and will be removed in RTK 2.0. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice"
)
}
}
}
const [
extraReducers = {},
actionMatchers = [],
Expand All @@ -333,12 +348,18 @@ export function createSlice<
: [options.extraReducers]

const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
return createReducer(
initialState,
finalCaseReducers as any,
actionMatchers,
defaultCaseReducer
)

return createReducer(initialState, (builder) => {
for (let key in finalCaseReducers) {
builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
}
for (let m of actionMatchers) {
builder.addMatcher(m.matcher, m.reducer)
}
if (defaultCaseReducer) {
builder.addDefaultCase(defaultCaseReducer)
}
})
}

let _reducer: ReducerWithInitialState<State>
Expand Down
45 changes: 45 additions & 0 deletions packages/toolkit/src/tests/createReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import type {
AnyAction,
} from '@reduxjs/toolkit'
import { createReducer, createAction, createNextState } from '@reduxjs/toolkit'
import {
mockConsole,
createConsole,
getLog,
} from 'console-testing-library/pure'

interface Todo {
text: string
Expand All @@ -29,7 +34,14 @@ type ToggleTodoReducer = CaseReducer<
PayloadAction<ToggleTodoPayload>
>

type CreateReducer = typeof createReducer

describe('createReducer', () => {
let restore: () => void

beforeEach(() => {
restore = mockConsole(createConsole())
})
describe('given impure reducers with immer', () => {
const addTodo: AddTodoReducer = (state, action) => {
const { newTodo } = action.payload
Expand All @@ -54,6 +66,39 @@ describe('createReducer', () => {
behavesLikeReducer(todosReducer)
})

describe('Deprecation warnings', () => {
let originalNodeEnv = process.env.NODE_ENV

beforeEach(() => {
jest.resetModules()
})

afterEach(() => {
process.env.NODE_ENV = originalNodeEnv
})

it('Warns about object notation deprecation, once', () => {
const { createReducer } = require('../createReducer')
let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})

expect(getLog().levels.warn).toMatch(
/The object notation for `createReducer` is deprecated/
)
restore = mockConsole(createConsole())

dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
expect(getLog().levels.warn).toBe('')
})

it('Does not warn in production', () => {
process.env.NODE_ENV = 'production'
const { createReducer } = require('../createReducer')
let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})

expect(getLog().levels.warn).toBe('')
})
})

describe('Immer in a production environment', () => {
let originalNodeEnv = process.env.NODE_ENV

Expand Down
68 changes: 66 additions & 2 deletions packages/toolkit/src/tests/createSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
getLog,
} from 'console-testing-library/pure'

type CreateSlice = typeof createSlice

describe('createSlice', () => {
let restore: () => void

beforeEach(() => {
restore = mockConsole(createConsole())
})

describe('when slice is undefined', () => {
it('should throw an error', () => {
expect(() =>
Expand Down Expand Up @@ -53,7 +55,9 @@ describe('createSlice', () => {
initialState: undefined,
})

expect(getLog().log).toBe('You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`')
expect(getLog().log).toBe(
'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`'
)
})
})

Expand Down Expand Up @@ -367,4 +371,64 @@ describe('createSlice', () => {
)
})
})

describe.only('Deprecation warnings', () => {
let originalNodeEnv = process.env.NODE_ENV

beforeEach(() => {
jest.resetModules()
restore = mockConsole(createConsole())
})

afterEach(() => {
process.env.NODE_ENV = originalNodeEnv
})

// NOTE: This needs to be in front of the later `createReducer` call to check the one-time warning
it('Warns about object notation deprecation, once', () => {
const { createSlice } = require('../createSlice')

let dummySlice = (createSlice as CreateSlice)({
name: 'dummy',
initialState: [],
reducers: {},
extraReducers: {
a: () => [],
},
})
// Have to trigger the lazy creation
let { reducer } = dummySlice
reducer(undefined, { type: 'dummy' })

expect(getLog().levels.warn).toMatch(
/The object notation for `createSlice.extraReducers` is deprecated/
)
restore = mockConsole(createConsole())

dummySlice = (createSlice as CreateSlice)({
name: 'dummy',
initialState: [],
reducers: {},
extraReducers: {
a: () => [],
},
})
reducer = dummySlice.reducer
reducer(undefined, { type: 'dummy' })
expect(getLog().levels.warn).toBe('')
})

it('Does not warn in production', () => {
process.env.NODE_ENV = 'production'
const { createSlice } = require('../createSlice')

let dummySlice = (createSlice as CreateSlice)({
name: 'dummy',
initialState: [],
reducers: {},
extraReducers: {},
})
expect(getLog().levels.warn).toBe('')
})
})
})

0 comments on commit a254fb5

Please sign in to comment.