Skip to content

Commit 24dbdf1

Browse files
authored
Merge pull request #2591 from reduxjs/feature/reducer-object-deprecation
2 parents aa7dafc + a254fb5 commit 24dbdf1

File tree

4 files changed

+161
-18
lines changed

4 files changed

+161
-18
lines changed

packages/toolkit/src/createReducer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
8080
getInitialState: () => S
8181
}
8282

83+
let hasWarnedAboutObjectNotation = false
84+
8385
/**
8486
* A utility function that allows defining a reducer as a mapping from action
8587
* type to *case reducer* functions that handle these action types. The
@@ -219,6 +221,17 @@ export function createReducer<S extends NotFunction<any>>(
219221
actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
220222
defaultCaseReducer?: CaseReducer<S>
221223
): ReducerWithInitialState<S> {
224+
if (process.env.NODE_ENV !== 'production') {
225+
if (typeof mapOrBuilderCallback === 'object') {
226+
if (!hasWarnedAboutObjectNotation) {
227+
hasWarnedAboutObjectNotation = true
228+
console.warn(
229+
"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"
230+
)
231+
}
232+
}
233+
}
234+
222235
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
223236
typeof mapOrBuilderCallback === 'function'
224237
? executeReducerBuilderCallback(mapOrBuilderCallback)

packages/toolkit/src/createSlice.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { executeReducerBuilderCallback } from './mapBuilders'
1919
import type { NoInfer } from './tsHelpers'
2020
import { freezeDraftable } from './utils'
2121

22+
let hasWarnedAboutObjectNotation = false
23+
2224
/**
2325
* An action creator attached to a slice.
2426
*
@@ -243,15 +245,16 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
243245
export type ValidateSliceCaseReducers<
244246
S,
245247
ACR extends SliceCaseReducers<S>
246-
> = ACR & {
247-
[T in keyof ACR]: ACR[T] extends {
248-
reducer(s: S, action?: infer A): any
248+
> = ACR &
249+
{
250+
[T in keyof ACR]: ACR[T] extends {
251+
reducer(s: S, action?: infer A): any
252+
}
253+
? {
254+
prepare(...a: never[]): Omit<A, 'type'>
255+
}
256+
: {}
249257
}
250-
? {
251-
prepare(...a: never[]): Omit<A, 'type'>
252-
}
253-
: {}
254-
}
255258

256259
function getType(slice: string, actionKey: string): string {
257260
return `${slice}/${actionKey}`
@@ -283,8 +286,10 @@ export function createSlice<
283286
typeof process !== 'undefined' &&
284287
process.env.NODE_ENV === 'development'
285288
) {
286-
if(options.initialState === undefined) {
287-
console.error('You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`')
289+
if (options.initialState === undefined) {
290+
console.error(
291+
'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`'
292+
)
288293
}
289294
}
290295

@@ -323,6 +328,16 @@ export function createSlice<
323328
})
324329

325330
function buildReducer() {
331+
if (process.env.NODE_ENV !== 'production') {
332+
if (typeof options.extraReducers === 'object') {
333+
if (!hasWarnedAboutObjectNotation) {
334+
hasWarnedAboutObjectNotation = true
335+
console.warn(
336+
"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"
337+
)
338+
}
339+
}
340+
}
326341
const [
327342
extraReducers = {},
328343
actionMatchers = [],
@@ -333,12 +348,18 @@ export function createSlice<
333348
: [options.extraReducers]
334349

335350
const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
336-
return createReducer(
337-
initialState,
338-
finalCaseReducers as any,
339-
actionMatchers,
340-
defaultCaseReducer
341-
)
351+
352+
return createReducer(initialState, (builder) => {
353+
for (let key in finalCaseReducers) {
354+
builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
355+
}
356+
for (let m of actionMatchers) {
357+
builder.addMatcher(m.matcher, m.reducer)
358+
}
359+
if (defaultCaseReducer) {
360+
builder.addDefaultCase(defaultCaseReducer)
361+
}
362+
})
342363
}
343364

344365
let _reducer: ReducerWithInitialState<State>

packages/toolkit/src/tests/createReducer.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import type {
66
AnyAction,
77
} from '@reduxjs/toolkit'
88
import { createReducer, createAction, createNextState } from '@reduxjs/toolkit'
9+
import {
10+
mockConsole,
11+
createConsole,
12+
getLog,
13+
} from 'console-testing-library/pure'
914

1015
interface Todo {
1116
text: string
@@ -29,7 +34,14 @@ type ToggleTodoReducer = CaseReducer<
2934
PayloadAction<ToggleTodoPayload>
3035
>
3136

37+
type CreateReducer = typeof createReducer
38+
3239
describe('createReducer', () => {
40+
let restore: () => void
41+
42+
beforeEach(() => {
43+
restore = mockConsole(createConsole())
44+
})
3345
describe('given impure reducers with immer', () => {
3446
const addTodo: AddTodoReducer = (state, action) => {
3547
const { newTodo } = action.payload
@@ -54,6 +66,39 @@ describe('createReducer', () => {
5466
behavesLikeReducer(todosReducer)
5567
})
5668

69+
describe('Deprecation warnings', () => {
70+
let originalNodeEnv = process.env.NODE_ENV
71+
72+
beforeEach(() => {
73+
jest.resetModules()
74+
})
75+
76+
afterEach(() => {
77+
process.env.NODE_ENV = originalNodeEnv
78+
})
79+
80+
it('Warns about object notation deprecation, once', () => {
81+
const { createReducer } = require('../createReducer')
82+
let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
83+
84+
expect(getLog().levels.warn).toMatch(
85+
/The object notation for `createReducer` is deprecated/
86+
)
87+
restore = mockConsole(createConsole())
88+
89+
dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
90+
expect(getLog().levels.warn).toBe('')
91+
})
92+
93+
it('Does not warn in production', () => {
94+
process.env.NODE_ENV = 'production'
95+
const { createReducer } = require('../createReducer')
96+
let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
97+
98+
expect(getLog().levels.warn).toBe('')
99+
})
100+
})
101+
57102
describe('Immer in a production environment', () => {
58103
let originalNodeEnv = process.env.NODE_ENV
59104

packages/toolkit/src/tests/createSlice.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {
66
getLog,
77
} from 'console-testing-library/pure'
88

9+
type CreateSlice = typeof createSlice
10+
911
describe('createSlice', () => {
1012
let restore: () => void
1113

1214
beforeEach(() => {
1315
restore = mockConsole(createConsole())
1416
})
15-
17+
1618
describe('when slice is undefined', () => {
1719
it('should throw an error', () => {
1820
expect(() =>
@@ -53,7 +55,9 @@ describe('createSlice', () => {
5355
initialState: undefined,
5456
})
5557

56-
expect(getLog().log).toBe('You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`')
58+
expect(getLog().log).toBe(
59+
'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`'
60+
)
5761
})
5862
})
5963

@@ -367,4 +371,64 @@ describe('createSlice', () => {
367371
)
368372
})
369373
})
374+
375+
describe.only('Deprecation warnings', () => {
376+
let originalNodeEnv = process.env.NODE_ENV
377+
378+
beforeEach(() => {
379+
jest.resetModules()
380+
restore = mockConsole(createConsole())
381+
})
382+
383+
afterEach(() => {
384+
process.env.NODE_ENV = originalNodeEnv
385+
})
386+
387+
// NOTE: This needs to be in front of the later `createReducer` call to check the one-time warning
388+
it('Warns about object notation deprecation, once', () => {
389+
const { createSlice } = require('../createSlice')
390+
391+
let dummySlice = (createSlice as CreateSlice)({
392+
name: 'dummy',
393+
initialState: [],
394+
reducers: {},
395+
extraReducers: {
396+
a: () => [],
397+
},
398+
})
399+
// Have to trigger the lazy creation
400+
let { reducer } = dummySlice
401+
reducer(undefined, { type: 'dummy' })
402+
403+
expect(getLog().levels.warn).toMatch(
404+
/The object notation for `createSlice.extraReducers` is deprecated/
405+
)
406+
restore = mockConsole(createConsole())
407+
408+
dummySlice = (createSlice as CreateSlice)({
409+
name: 'dummy',
410+
initialState: [],
411+
reducers: {},
412+
extraReducers: {
413+
a: () => [],
414+
},
415+
})
416+
reducer = dummySlice.reducer
417+
reducer(undefined, { type: 'dummy' })
418+
expect(getLog().levels.warn).toBe('')
419+
})
420+
421+
it('Does not warn in production', () => {
422+
process.env.NODE_ENV = 'production'
423+
const { createSlice } = require('../createSlice')
424+
425+
let dummySlice = (createSlice as CreateSlice)({
426+
name: 'dummy',
427+
initialState: [],
428+
reducers: {},
429+
extraReducers: {},
430+
})
431+
expect(getLog().levels.warn).toBe('')
432+
})
433+
})
370434
})

0 commit comments

Comments
 (0)