Skip to content
This repository has been archived by the owner on Jan 16, 2024. It is now read-only.

Commit

Permalink
provide original store slice state before in went through Tesler redu…
Browse files Browse the repository at this point in the history
…cer as a reducer argument to allow overriding built-in behavior
  • Loading branch information
Dergash committed Feb 3, 2021
1 parent 94cb1a7 commit 910414b
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 76 deletions.
78 changes: 6 additions & 72 deletions src/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import React from 'react'
import { Action, applyMiddleware, compose, createStore, Middleware, Store, StoreCreator } from 'redux'
import { Action, Store } from 'redux'
import { Provider as ReduxProvider } from 'react-redux'
import { createEpicMiddleware, Epic, combineEpics as legacyCombineEpics } from 'redux-observable'
import { reducers as coreReducers } from './reducers/index'
import { Route } from './interfaces/router'
import { ClientReducersMapObject, CombinedReducersMapObject, CoreReducer, Store as CoreStore } from './interfaces/store'
import { ClientReducersMapObject, Store as CoreStore } from './interfaces/store'
import { Location } from 'history'
import { AnyAction } from './actions/actions'
import { AxiosInstance } from 'axios'
import { initHistory } from './reducers/router'
import { combineReducers } from './utils/redux'
import { initLocale } from './imports/i18n'
import { Resource, i18n } from 'i18next'
import CustomEpics, { isLegacyCustomEpics, AnyEpic } from './interfaces/customEpics'
import combineEpics from './utils/combineEpics'
import { legacyCoreEpics } from './epics'
import { combineMiddlewares } from './utils/combineMiddlewares'
import { middlewares as coreMiddlewares } from './middlewares'
import CustomEpics, { AnyEpic } from './interfaces/customEpics'
import { CustomMiddlewares } from './interfaces/customMiddlewares'
import { defaultBuildLocation, defaultParseLocation } from './utils/history'
import { configureStore } from './utils/configureStore'

export interface ProviderProps<ClientState, ClientActions> {
children: React.ReactNode
Expand All @@ -39,19 +32,10 @@ export interface ProviderProps<ClientState, ClientActions> {
*/
export let store: Store<CoreStore> = null
export let axiosInstance: AxiosInstance = null
export let parseLocation: (loc: Location<any>) => Route = null
export let parseLocation: (loc: Location<any>) => Route = defaultParseLocation
export let buildLocation: (route: Route) => string = null
export let localeProviderInstance: i18n = null

/**
* TODO
*
* @param storeCreator
*/
function withLogger(storeCreator: StoreCreator): StoreCreator {
return (window as any).devToolsExtension ? (window as any).devToolsExtension()(storeCreator) : storeCreator
}

/**
* TODO
*
Expand Down Expand Up @@ -91,56 +75,6 @@ export function getLocaleProviderInstance() {
return localeProviderInstance
}

/**
* TODO
*
* @param customReducers
* @param customEpics
* @param useEpics
* @param customMiddlewares
*/
export function configureStore<ClientState, ClientActions extends Action<any>>(
customReducers = {} as ClientReducersMapObject<ClientState, ClientActions>,
customEpics: CustomEpics | Epic<any, ClientState> = null,
useEpics = true,
customMiddlewares: CustomMiddlewares = null
): Store<ClientState & CoreStore> {
type CombinedActions = AnyAction & ClientActions
// If core reducer slices have a matching client app reducer slice
// launch the core first and then client
// TODO: Extract this to an utility
const reducers = { ...coreReducers } as CombinedReducersMapObject<CoreStore & ClientState, CombinedActions>
Object.keys(customReducers).forEach((reducerName: Extract<keyof ClientState, string>) => {
const coreInitialState = coreReducers[reducerName]?.(undefined, { type: ' UNKNOWN ACTION ' })
const reducerInitialState = {
...(coreInitialState || ({} as ClientState)),
...customReducers[reducerName].initialState
}

if (reducers[reducerName as keyof ClientState] && !customReducers[reducerName].override) {
const combined: CoreReducer<ClientState[keyof ClientState], CombinedActions> = (
state = reducerInitialState,
action,
getStore
) => {
const storeAfterCore = coreReducers[reducerName](state, action, getStore)
return customReducers[reducerName as keyof ClientState].reducer(storeAfterCore, action, getStore)
}
reducers[reducerName as keyof ClientState] = combined
} else {
reducers[reducerName as keyof ClientState] = customReducers[reducerName].reducer
}
})

const middlewares: Middleware[] = combineMiddlewares(coreMiddlewares, customMiddlewares)

if (useEpics) {
const epics = isLegacyCustomEpics(customEpics) ? legacyCombineEpics(legacyCoreEpics, customEpics) : combineEpics(customEpics)
middlewares.push(createEpicMiddleware(epics))
}
return compose(applyMiddleware(...middlewares))(withLogger(createStore))(combineReducers(reducers))
}

/**
*
* @param props
Expand All @@ -155,7 +89,7 @@ const Provider = <ClientState extends Partial<CoreStore>, ClientActions extends
if (props.axiosInstance) {
axiosInstance = props.axiosInstance
}
parseLocation = props.parseLocation || defaultParseLocation
parseLocation = props.parseLocation
buildLocation = props.buildLocation || defaultBuildLocation
return <ReduxProvider store={store}>{props.children}</ReduxProvider>
}
Expand Down
1 change: 0 additions & 1 deletion src/epics/router/__tests__/changeLocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ describe('selectScreenFail', () => {
store.getState().router.viewName = 'view-next'
const epic = changeLocation(ActionsObservable.of(action), store)
testEpic(epic, res => {
console.warn(res)
expect(res.length).toBe(3)
expect(res[0]).toEqual(
expect.objectContaining(
Expand Down
30 changes: 28 additions & 2 deletions src/interfaces/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,44 @@ export interface Store {
view: ViewState
data: DataState
depthData: DepthDataState
[reducerName: string]: any // TODO: Исправить комбинирование редьюсеров и убрать
[reducerName: string]: any // TODO: Fix how reducers are combined and remove
}

export type CoreReducer<ReducerState, ClientActions, State = Store> = (
/**
* The state of Redux store slice
*/
state: ReducerState,
/**
* Redux action
*/
action: AnyAction & ClientActions,
store?: Readonly<State>
/**
* Allows direct access to other slices of redux store from the reducer
*/
store?: Readonly<State>,
/**
* The original state of Redux store slice before in went through Tesler reducer;
*
* Can be used to implement your own logic in client application reducer for built-in action.
*/
originalState?: ReducerState
) => ReducerState

export interface ClientReducer<ReducerState, ClientActions> {
/**
* Initial state for Redux store slice; will replace built-in Tesler initial state for matching slice
*/
initialState: ReducerState
/**
* If true than custom reducer will replace built-in Tesler reducer for this store slice
*
* @deprecated TODO: This functionality is conceptionally flawed and will be removed in 2.0.0
*/
override?: boolean
/**
* Reducer function for specific store slice
*/
reducer: CoreReducer<ReducerState, ClientActions>
}

Expand Down
2 changes: 1 addition & 1 deletion src/reducers/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const initialState: Route = { type: RouteType.default, path: '/', params: null,
export function router(state: Route = initialState, action: AnyAction): Route {
switch (action.type) {
case types.loginDone:
return parseLocation(historyObj.location)
return parseLocation(historyObj?.location)
case types.changeLocation:
const rawLocation = action.payload.rawLocation
if (rawLocation != null) {
Expand Down
60 changes: 60 additions & 0 deletions src/utils/__tests__/configureStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { $do } from '../../actions/actions'
import { configureStore } from '../configureStore'
import * as router from '../../Provider'

jest.spyOn(router, 'parseLocation').mockImplementation(() => {
return { screenName: null, viewName: null, type: null, path: null, params: null }
})

describe('configureStore', () => {
it('handles built-in actions by built-in reducers', () => {
const store = configureStore({}, null, false, null)
expect(store.getState().session.active).toBe(false)
store.dispatch($do.loginDone({ screens: null }))
expect(store.getState().session.active).toBe(true)
})

it('applies custom reducer after Tesler built-in reducer', () => {
const mock = jest.fn()
const storeInstance = configureStore(
{
session: {
initialState: {},
reducer: (state, action, store, originalState) => {
mock('success')
return state
}
}
},
null,
false,
null
)
expect(storeInstance.getState().session.active).toBe(false)
storeInstance.dispatch($do.loginDone({ screens: null }))
expect(storeInstance.getState().session.active).toBe(true)
expect(mock).toBeCalledWith('success')
})

it('allows custom reducer to override built-in implementation ', () => {
const mock = jest.fn()
const storeInstance = configureStore(
{
session: {
initialState: {},
reducer: (state, action, store, originalState) => {
mock('success')
return originalState
}
}
},
null,
false,
null
)
expect(storeInstance.getState().session.active).toBe(false)
storeInstance.dispatch($do.loginDone({ screens: null }))
expect(storeInstance.getState().session.active).toBe(false)
expect(mock).toBeCalledWith('success')
})
})
72 changes: 72 additions & 0 deletions src/utils/configureStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Action, applyMiddleware, compose, createStore, Middleware, Store, StoreCreator } from 'redux'
import { createEpicMiddleware, Epic, combineEpics as legacyCombineEpics } from 'redux-observable'
import { reducers as coreReducers } from '../reducers/index'
import combineEpics from '../utils/combineEpics'
import { legacyCoreEpics } from '../epics'
import { combineMiddlewares } from '../utils/combineMiddlewares'
import { middlewares as coreMiddlewares } from '../middlewares'
import CustomEpics, { isLegacyCustomEpics } from '../interfaces/customEpics'
import { combineReducers } from '../utils/redux'
import { AnyAction } from '../actions/actions'
import { ClientReducersMapObject, CombinedReducersMapObject, CoreReducer, Store as CoreStore } from '../interfaces/store'
import { CustomMiddlewares } from '../interfaces/customMiddlewares'

/**
* TODO
*
* @param storeCreator
*/
function withLogger(storeCreator: StoreCreator): StoreCreator {
return (window as any).devToolsExtension ? (window as any).devToolsExtension()(storeCreator) : storeCreator
}

/**
* Configures Redux store by apply redux-observable epic middleware and custom version of `combineReducers` function
*
* @param customReducers Client application reducers
* @param customEpics Client application epics
* @param useEpics Can be set to `false` if client application does not provide redux-observable peer dependency
* and does not rely on Tesler epics (e.g. importing only UI components)
* @param customMiddlewares Any additional middlewares provided by client application
*/
export function configureStore<ClientState, ClientActions extends Action<any>>(
customReducers = {} as ClientReducersMapObject<ClientState, ClientActions>,
customEpics: CustomEpics | Epic<any, ClientState> = null,
useEpics = true,
customMiddlewares: CustomMiddlewares = null
): Store<ClientState & CoreStore> {
type CombinedActions = AnyAction & ClientActions
// If core reducer slices have a matching client app reducer slice
// launch the core first and then client
// TODO: Extract this to an utility
const reducers = { ...coreReducers } as CombinedReducersMapObject<CoreStore & ClientState, CombinedActions>
Object.keys(customReducers).forEach((reducerName: Extract<keyof ClientState, string>) => {
const coreInitialState = coreReducers[reducerName]?.(undefined, { type: ' UNKNOWN ACTION ' })
const reducerInitialState = {
...(coreInitialState || ({} as ClientState)),
...customReducers[reducerName].initialState
}

if (reducers[reducerName as keyof ClientState] && !customReducers[reducerName].override) {
const combined: CoreReducer<ClientState[keyof ClientState], CombinedActions> = (
state = reducerInitialState,
action,
getStore
) => {
const storeAfterCore = coreReducers[reducerName](state, action, getStore)
return customReducers[reducerName as keyof ClientState].reducer(storeAfterCore, action, getStore, state)
}
reducers[reducerName as keyof ClientState] = combined
} else {
reducers[reducerName as keyof ClientState] = customReducers[reducerName].reducer
}
})

const middlewares: Middleware[] = combineMiddlewares(coreMiddlewares, customMiddlewares)

if (useEpics) {
const epics = isLegacyCustomEpics(customEpics) ? legacyCombineEpics(legacyCoreEpics, customEpics) : combineEpics(customEpics)
middlewares.push(createEpicMiddleware(epics))
}
return compose(applyMiddleware(...middlewares))(withLogger(createStore))(combineReducers(reducers))
}

0 comments on commit 910414b

Please sign in to comment.