From a254fb54a79a761d7b3be12fb08cd0800e421978 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 13 Aug 2022 21:29:46 -0400 Subject: [PATCH] Add one-time runtime deprecation warnings for reducer object notation --- packages/toolkit/src/createReducer.ts | 13 ++++ packages/toolkit/src/createSlice.ts | 53 ++++++++++----- .../toolkit/src/tests/createReducer.test.ts | 45 ++++++++++++ .../toolkit/src/tests/createSlice.test.ts | 68 ++++++++++++++++++- 4 files changed, 161 insertions(+), 18 deletions(-) diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index 4170e956a0..28a62c6225 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -80,6 +80,8 @@ export type ReducerWithInitialState> = Reducer & { 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 @@ -219,6 +221,17 @@ export function createReducer>( actionMatchers: ReadonlyActionMatcherDescriptionCollection = [], defaultCaseReducer?: CaseReducer ): ReducerWithInitialState { + 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) diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index 60376e1e3f..8406a5b98e 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -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. * @@ -243,15 +245,16 @@ type SliceDefinedCaseReducers> = { export type ValidateSliceCaseReducers< S, ACR extends SliceCaseReducers -> = 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 + } + : {} } - ? { - prepare(...a: never[]): Omit - } - : {} -} function getType(slice: string, actionKey: string): string { return `${slice}/${actionKey}` @@ -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`' + ) } } @@ -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 = [], @@ -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) + } + for (let m of actionMatchers) { + builder.addMatcher(m.matcher, m.reducer) + } + if (defaultCaseReducer) { + builder.addDefaultCase(defaultCaseReducer) + } + }) } let _reducer: ReducerWithInitialState diff --git a/packages/toolkit/src/tests/createReducer.test.ts b/packages/toolkit/src/tests/createReducer.test.ts index 2356d20918..5c3a181168 100644 --- a/packages/toolkit/src/tests/createReducer.test.ts +++ b/packages/toolkit/src/tests/createReducer.test.ts @@ -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 @@ -29,7 +34,14 @@ type ToggleTodoReducer = CaseReducer< PayloadAction > +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 @@ -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 diff --git a/packages/toolkit/src/tests/createSlice.test.ts b/packages/toolkit/src/tests/createSlice.test.ts index a9edd50887..bea220ba6a 100644 --- a/packages/toolkit/src/tests/createSlice.test.ts +++ b/packages/toolkit/src/tests/createSlice.test.ts @@ -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(() => @@ -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`' + ) }) }) @@ -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('') + }) + }) })