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 5193835 commit 3b97d22
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 125 deletions.
35 changes: 11 additions & 24 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 @@ -42,7 +41,7 @@ import { kindOf } from './utils/kindOf'
export function createStore<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
/**
* @deprecated
*
Expand Down Expand Up @@ -72,12 +71,12 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
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 function createStore<S, A extends Action, Ext = {}, StateExt = never>(
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 @@ -114,7 +113,7 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
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 @@ -288,11 +287,8 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
* 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 @@ -301,22 +297,13 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
)
}

// 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 @@ -374,7 +361,7 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>(
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
}

Expand Down Expand Up @@ -416,7 +403,7 @@ export function legacy_createStore<
>(
reducer: Reducer<S, A>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
): Store<S, A, StateExt> & Ext
/**
* Creates a Redux store that holds the state tree.
*
Expand Down Expand Up @@ -456,7 +443,7 @@ export function legacy_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 function legacy_createStore<
S,
A extends Action,
Expand All @@ -466,6 +453,6 @@ export function legacy_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 {
return createStore(reducer, preloadedState as any, enhancer)
}
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
25 changes: 16 additions & 9 deletions test/combineReducers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* 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 '..'

describe('Utils', () => {
Expand Down Expand Up @@ -327,31 +328,35 @@ 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 State = { foo: {}; bar?: {} }

const originalCompositeReducer = combineReducers<State>({ foo })
const store = createStore(originalCompositeReducer)

store.dispatch(ACTION)

const initialState = store.getState()

store.replaceReducer(combineReducers({ foo, bar }))
store.replaceReducer(combineReducers<State>({ 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 State = { foo?: {}; bar: {}; baz?: {} }

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

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

store.dispatch(ACTION)

const initialState = store.getState()

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

const nextState = store.getState()
Expand All @@ -374,7 +379,9 @@ describe('Utils', () => {
})

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 = combineReducers<{ foo?: {}; bar: {} }>(
{ foo, bar }
)
const store = createStore(originalCompositeReducer)

store.dispatch(ACTION)
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)
}
18 changes: 0 additions & 18 deletions test/replaceReducers.spec.ts

This file was deleted.

Loading

0 comments on commit 3b97d22

Please sign in to comment.