Skip to content

Commit

Permalink
Revert adding the store as a return type to replaceReducer
Browse files Browse the repository at this point in the history
  • Loading branch information
Methuselah96 committed Jan 28, 2023
1 parent 6294ad2 commit ebe6915
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 125 deletions.
29 changes: 8 additions & 21 deletions src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
PreloadedState,
StoreEnhancer,
Dispatch,
Observer,
ExtendState
Observer
} from './types/store'
import { Action } from './types/actions'
import { Reducer } from './types/reducers'
Expand Down Expand Up @@ -47,7 +46,7 @@ export default function createStore<
>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
): Store<S, A, StateExt> & Ext
export default function createStore<
S,
A extends Action,
Expand All @@ -57,7 +56,7 @@ export default function createStore<
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
): Store<S, A, StateExt> & Ext
export default function createStore<
S,
A extends Action,
Expand All @@ -67,7 +66,7 @@ export default function createStore<
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
): Store<S, A, StateExt> & Ext {
if (typeof reducer !== 'function') {
throw new Error(
`Expected the root reducer to be a function. Instead, received: '${kindOf(
Expand Down Expand Up @@ -104,7 +103,7 @@ export default function createStore<
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
) as Store<S, A, StateExt> & Ext
}

let currentReducer = reducer
Expand Down Expand Up @@ -278,11 +277,8 @@ export default function createStore<
* implement a hot reloading mechanism for Redux.
*
* @param nextReducer The reducer for the store to use instead.
* @returns The same store instance with a new reducer in place.
*/
function replaceReducer<NewState, NewActions extends A>(
nextReducer: Reducer<NewState, NewActions>
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
function replaceReducer(nextReducer: Reducer<S, A>): void {
if (typeof nextReducer !== 'function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf(
Expand All @@ -291,22 +287,13 @@ export default function createStore<
)
}

// TODO: do this more elegantly
;(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer
currentReducer = nextReducer

// This action has a similar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE } as A)
// change the type of the store by casting it to the new store
return store as unknown as Store<
ExtendState<NewState, StateExt>,
NewActions,
StateExt,
Ext
> &
Ext
}

/**
Expand Down Expand Up @@ -364,6 +351,6 @@ export default function createStore<
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
} as unknown as Store<S, A, StateExt> & Ext
return store
}
18 changes: 7 additions & 11 deletions src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,11 @@ export type Observer<T> = {
* @template S The type of state held by this store.
* @template A the type of actions which may be dispatched by this store.
* @template StateExt any extension to state from store enhancers
* @template Ext any extensions to the store from store enhancers
*/
export interface Store<
S = any,
A extends Action = AnyAction,
StateExt = never,
Ext = {}
StateExt = never
> {
/**
* Dispatches an action. It is the only way to trigger a state change.
Expand Down Expand Up @@ -172,7 +170,7 @@ export interface Store<
*
* @returns The current state tree of your application.
*/
getState(): S
getState(): ExtendState<S, StateExt>

/**
* Adds a change listener. It will be called any time an action is
Expand Down Expand Up @@ -209,17 +207,15 @@ export interface Store<
*
* @param nextReducer The reducer for the store to use instead.
*/
replaceReducer<NewState, NewActions extends Action>(
nextReducer: Reducer<NewState, NewActions>
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext
replaceReducer(nextReducer: Reducer<S, A>): void

/**
* Interoperability point for observable/reactive libraries.
* @returns {observable} A minimal observable of state changes.
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
[Symbol.observable](): Observable<S>
[Symbol.observable](): Observable<ExtendState<S, StateExt>>
}

/**
Expand All @@ -237,12 +233,12 @@ export interface StoreCreator {
<S, A extends Action, Ext = {}, StateExt = never>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
): Store<S, A, StateExt> & Ext
<S, A extends Action, Ext = {}, StateExt = never>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
): Store<S, A, StateExt> & Ext
}

/**
Expand Down Expand Up @@ -275,4 +271,4 @@ export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
) => Store<S, A, StateExt> & Ext
58 changes: 46 additions & 12 deletions test/combineReducers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable no-console */
import {
createStore,
combineReducers,
Reducer,
__DO_NOT_USE__ActionTypes as ActionTypes,
Action,
AnyAction,
__DO_NOT_USE__ActionTypes as ActionTypes
combineReducers,
createStore,
Reducer
} from '..'
import { combineReducersAccuratelyTyped } from './helpers/combineReducersTypes'

describe('Utils', () => {
describe('combineReducers', () => {
Expand Down Expand Up @@ -327,61 +329,93 @@ describe('Utils', () => {
const ACTION = { type: 'ACTION' }

it('should return an updated state when additional reducers are passed to combineReducers', function () {
const originalCompositeReducer = combineReducers({ foo })
type Reducers = {
foo: Reducer<{}, Action<unknown>>
bar?: Reducer<{}, Action<unknown>>
}

const originalCompositeReducer =
combineReducersAccuratelyTyped<Reducers>({ foo })
const store = createStore(originalCompositeReducer)

store.dispatch(ACTION)

const initialState = store.getState()

store.replaceReducer(combineReducers({ foo, bar }))
store.replaceReducer(
combineReducersAccuratelyTyped<Reducers>({
foo,
bar
})
)
store.dispatch(ACTION)

const nextState = store.getState()
expect(nextState).not.toBe(initialState)
})

it('should return an updated state when reducers passed to combineReducers are changed', function () {
type Reducers = {
foo?: Reducer<{}, Action<unknown>>
bar: Reducer<{}, Action<unknown>>
baz?: Reducer<{}, Action<unknown>>
}

const baz = (state = {}) => state

const originalCompositeReducer = combineReducers({ foo, bar })
const originalCompositeReducer =
combineReducersAccuratelyTyped<Reducers>({
foo,
bar
})
const store = createStore(originalCompositeReducer)

store.dispatch(ACTION)

const initialState = store.getState()

store.replaceReducer(combineReducers({ baz, bar }))
store.replaceReducer(
combineReducersAccuratelyTyped<Reducers>({ baz, bar })
)
store.dispatch(ACTION)

const nextState = store.getState()
expect(nextState).not.toBe(initialState)
})

it('should return the same state when reducers passed to combineReducers not changed', function () {
const originalCompositeReducer = combineReducers({ foo, bar })
const originalCompositeReducer = combineReducersAccuratelyTyped({
foo,
bar
})
const store = createStore(originalCompositeReducer)

store.dispatch(ACTION)

const initialState = store.getState()

store.replaceReducer(combineReducers({ foo, bar }))
store.replaceReducer(combineReducersAccuratelyTyped({ foo, bar }))
store.dispatch(ACTION)

const nextState = store.getState()
expect(nextState).toBe(initialState)
})

it('should return an updated state when one of more reducers passed to the combineReducers are removed', function () {
const originalCompositeReducer = combineReducers({ foo, bar })
const originalCompositeReducer = combineReducersAccuratelyTyped<{
foo?: Reducer<{}, Action<unknown>>
bar: Reducer<{}, Action<unknown>>
}>({
foo,
bar
})
const store = createStore(originalCompositeReducer)

store.dispatch(ACTION)

const initialState = store.getState()

store.replaceReducer(combineReducers({ bar }))
store.replaceReducer(combineReducersAccuratelyTyped({ bar }))

const nextState = store.getState()
expect(nextState).not.toBe(initialState)
Expand Down
32 changes: 27 additions & 5 deletions test/createStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { createStore, combineReducers, StoreEnhancer, Action, Store } from '..'
import {
createStore,
combineReducers,
StoreEnhancer,
Action,
Store,
Reducer
} from '..'
import {
addTodo,
dispatchInMiddle,
Expand All @@ -12,6 +19,10 @@ import * as reducers from './helpers/reducers'
import { from, ObservableInput } from 'rxjs'
import { map } from 'rxjs/operators'
import $$observable from '../src/utils/symbol-observable'
import {
combineReducersAccuratelyTyped,
ReducerThatAllowsForPartialInputState
} from './helpers/combineReducersTypes'

describe('createStore', () => {
it('exposes the public API', () => {
Expand Down Expand Up @@ -823,19 +834,30 @@ describe('createStore', () => {
const originalConsoleError = console.error
console.error = jest.fn()

type YState = { z: number; w?: number }

type Reducers = {
x?: Reducer<number, Action<unknown>>
y: ReducerThatAllowsForPartialInputState<
YState,
Action<unknown>,
Partial<YState> | undefined
>
}

const store = createStore(
combineReducers({
combineReducersAccuratelyTyped<Reducers>({
x: (s = 0, _) => s,
y: combineReducers({
y: combineReducersAccuratelyTyped({
z: (s = 0, _) => s,
w: (s = 0, _) => s
})
})
)

store.replaceReducer(
combineReducers({
y: combineReducers({
combineReducersAccuratelyTyped({
y: combineReducersAccuratelyTyped({
z: (s = 0, _) => s
})
})
Expand Down
59 changes: 59 additions & 0 deletions test/helpers/combineReducersTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Action,
ActionFromReducersMapObject,
combineReducers,
Reducer,
ReducersMapObject
} from '../..'

/**
* The existing `Reducer` type assumes that the input state is `S` or `undefined`. This works for most cases, but
* `combineReducers` allows for properties in the input state to be missing. This type allows for that scenario and is
* used by `combineReducersAccuratelyTyped`. It can be removed if the `Reducer` type in the core library is updated to
* allow for this scenario.
*/
export type ReducerThatAllowsForPartialInputState<
S extends InputState,
A extends Action<unknown>,
InputState
> = (state: InputState, action: A) => S

type ReducersMapObjectThatAllowsForPartialInputState<
S extends InputState,
A extends Action<unknown>,
InputState
> = {
[K in keyof InputState]: ReducerThatAllowsForPartialInputState<
S[K],
A,
InputState[K]
>
}

type StateFromReducersMapObjectThatAllowsForPartialInputState<
M extends ReducersMapObjectThatAllowsForPartialInputState<
unknown,
Action<unknown>,
unknown
>
> = {
[P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never
}

/**
* The `combineReducers` function creates a reducer that allows for missing properties in the input state at the
* top-level, but the types do not accurately reflect that. This method provides accurate types for
* `combineReducers` in order to get the tests that use `replaceReducer` to type-check successfully. This can be
* removed if the types for `combineReducers` are fixed.
*/
export function combineReducersAccuratelyTyped<M extends ReducersMapObject>(
reducers: M
): (
state:
| Partial<StateFromReducersMapObjectThatAllowsForPartialInputState<M>>
| undefined,
action: ActionFromReducersMapObject<M>
) => StateFromReducersMapObjectThatAllowsForPartialInputState<M> {
// @ts-ignore
return combineReducers(reducers)
}
Loading

0 comments on commit ebe6915

Please sign in to comment.