Skip to content

Commit

Permalink
Extend store object to return mapStoreToProps and connected
Browse files Browse the repository at this point in the history
  • Loading branch information
cimdalli committed Nov 14, 2019
1 parent c4258e7 commit ed6d414
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 59 deletions.
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,18 @@ type StoreState = { layout: LayoutState }
const switchTheme = createAction('switchTheme')

// Build reducer
const layoutReducer = new ReducerBuilder<LayoutState>()
.handle(switchTheme, (state, action) => {
const layoutReducer = new ReducerBuilder<LayoutState>().handle(
switchTheme,
(state, action) => {
const isDark = !state.layout.isDark
return { ...state, isDark }
},
)

// Build store
export const [store, mapStoreToProps] = new StoreBuilder<StoreState>()
export const { mapStoreToProps, connected, ...store } = new StoreBuilder<
StoreState
>()
.withReducerBuildersMap({ layout: layoutReducer })
.withDevTools() // enable chrome devtools
.build()
Expand All @@ -56,14 +59,7 @@ export const [store, mapStoreToProps] = new StoreBuilder<StoreState>()
```tsx
import React from 'react'
import { mapDispatchToProps } from 'redux-ts'
import { mapStoreToProps, store } from './store'

// Connect store
const Root: React.FC = props => (
<Provider store={store}>
<Main />
</Provider>
)
import { connected, mapStoreToProps, store } from './store'

// Map store to component props
const storeProps = mapStoreToProps(store => ({
Expand All @@ -74,11 +70,21 @@ const storeProps = mapStoreToProps(store => ({
const dispatchProps = mapDispatchToProps({ switchTheme })

// Connect component
const Main = connect(storeProps, dispatchProps)(({ theme, switchTheme }) => (
const ConnectedMain = connected(
storeProps,
dispatchProps,
)(({ theme, switchTheme }) => (
<div>
<span>Current theme: {theme}</span>
<button onClick={switchTheme}>Switch theme</button>
</div>
))

// Connect store
const Root: React.FC = props => (
<Provider store={store}>
<ConnectedMain />
</Provider>
)

ReactDOM.render(<Root />, document.getElementById('app'))
Expand All @@ -97,7 +103,7 @@ import { connectRouter, routerMiddleware } from 'connected-react-router'

export const history = createBrowserHistory()
const routerReducer = connectRouter(history)
export const [store] = new StoreBuilder<StoreState>()
export const store = new StoreBuilder<StoreState>()
.withMiddleware(routerMiddleware(history))
.withReducer('router', routerReducer)
.withDevTools() // enable chrome devtools
Expand Down Expand Up @@ -135,7 +141,7 @@ Create redux store with builder pattern.
import { StoreBuilder } from 'redux-ts'
import { authReducer } from './reducers/authReducer'

export const [store, mapStoreToProps] = new StoreBuilder<StoreState>()
export const { connected, mapStoreToProps, ...store } = new StoreBuilder<StoreState>()
.withInitialState({test:true})
.withMiddleware()
.withReducer("auth", authReducer)
Expand All @@ -147,6 +153,7 @@ export const [store, mapStoreToProps] = new StoreBuilder<StoreState>()
- As generic parameter, it requires store state type in order to match given reducers and the state.
- Any number of middleware, enhancer or reducer can be used to build the state.
- `mapStoreToProps` is a dummy method that returns passed parameter again. This method can be used to map store object to props which are consumed from connected components. Return type is `MapStateToPropsParam` which is compatible with [connect](https://react-redux.js.org/api/connect).
- `connected` function is also another dummy function that wraps original [connect](https://react-redux.js.org/api/connect) function but with implicit type resolution support. Problem with original one, when you connect your component with connect method, it is trying to resolve typing by matching signature you passed as parameters to connect _(mapStateToProps, mapDispatchToProps)_ and component own properties. If you are using explicit typing mostly, it is totally fine to use original one. But if you are expecting implicit type resolution, original connect is failing and resolving inner component type as `never`.

### Actions

Expand Down Expand Up @@ -231,14 +238,17 @@ export const authReducer = new ReducerBuilder<AuthState>()

[connect](https://react-redux.js.org/api/connect) method is part of [react-redux](https://github.com/reduxjs/react-redux) library that allows you to connect your react components with redux store.

> You can use `connected` method for implicit type resolution.
```tsx
import * as React from 'react'
import { connect } from 'react-redux'
import { mapDispatchToProps } from 'redux-ts'
import { mapStoreToProps } from '../store'
import { store } from '../store'
import { ChangeTheme } from '../actions/layout.actions'
import { Logout } from '../actions/auth.actions'

const { mapStoreToProps, connected } = store

// Map store object to component props
const storeProps = mapStoreToProps(store => ({
useDarkTheme:!!store.layout.useDarkTheme
Expand All @@ -250,7 +260,7 @@ const dispatchProps = mapDispatchToProps({
ChangeTheme
})

export const Layout = connect(storeProps, dispatchProps)(({
export const Layout = connected(storeProps, dispatchProps)(({
children, // standard react prop
useDarkTheme, // mapped store prop
Logout, // mapped action prop
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-ts",
"version": "4.2.0-rc.3",
"version": "4.2.0-rc.6",
"description": "Utils to define redux reducer/action in typescript",
"main": "dist/redux-ts.production.min.js",
"types": "lib/index.d.ts",
Expand All @@ -21,7 +21,7 @@
"build:prod": "webpack --config webpack.prod.js",
"clean": "rimraf dist lib",
"build": "npm run clean && npm run build:dev && npm run build:prod",
"prepublish": "npm run test && npm run build",
"prepare": "npm run test && npm run build",
"test": "mocha --require source-map-support/register --require ts-node/register tests/**/*.spec.ts"
},
"tags": [
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/redux.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionCreatorDefinition, DispatchToProps, Indexer } from '..'
import { ActionCreatorDefinition, DispatchToProps, Indexer } from '../'

export const createAction = <TPayload = any, TMeta = any>(
type: string,
Expand Down Expand Up @@ -35,4 +35,4 @@ export const mapDispatchToProps: DispatchToProps = map => (dispatch, own) => {
) as typeof m

return typeof map === 'function' ? mapper(map(dispatch, own)) : mapper(map)
}
}
44 changes: 42 additions & 2 deletions src/models/redux.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { MapDispatchToPropsFunction, MapStateToProps } from 'react-redux'
import { AnyAction } from 'redux'
import {
MapDispatchToPropsFunction, MapDispatchToPropsParam, MapStateToProps, MapStateToPropsParam
} from 'react-redux'
import { AnyAction, Store as ReduxStore } from 'redux'

export type HOC<Pin, Pout> = (c: React.ComponentType<Pin>) => React.ComponentType<Pout>

export type Indexer<T = any> = { [key: string]: T }

Expand All @@ -14,3 +18,39 @@ export type DispatchToProps = {
map: T | MapDispatchToPropsFunction<T, TOwn>,
): MapDispatchToPropsFunction<T, TOwn>
}

export interface Store<StoreState> extends ReduxStore<StoreState> {
/**
* Dummy function to return `MapStateToProps` type that can be passed to `connect`
* As paramter, mapper function is required which takes store object and returns indexer object
* You can expose that function from your store object to be able to use on connected components.
* ex.
* export const [store, mapStoreToProps] = new StoreBuilder<StoreState>().build()
*
* @type {StateToProps<S>}
* @memberof StoreBuilder
*/
mapStoreToProps: StateToProps<StoreState>

/**
* Wrapper of redux connect method in order to support inferring props of inner component
* ex.
* export const [store, mapStoreToProps, connected] = new StoreBuilder<StoreState>().build()
*
* @private
* @template TStateProps
* @template TDispatchProps
* @template TOwnProps
* @template State
* @param {MapStateToPropsParam<TStateProps, TOwnProps, State>} [mapStateToProps]
* @param {MapDispatchToPropsParam<TDispatchProps, TOwnProps>} [mapDispatchToProps]
* @returns {(HOC<
* TStateProps & TDispatchProps & TOwnProps,
* Exclude<TOwnProps, TStateProps & TDispatchProps>>)}
* @memberof StoreBuilder
*/
connected<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, StoreState>,
mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
): HOC<TStateProps & TDispatchProps & TOwnProps, Exclude<TOwnProps, TStateProps & TDispatchProps>>
}
40 changes: 13 additions & 27 deletions src/store.builder.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { connect } from 'react-redux'
import {
applyMiddleware,
combineReducers,
compose,
createStore,
DeepPartial,
Dispatch,
Middleware,
Reducer,
ReducersMapObject,
Store,
StoreEnhancer,
applyMiddleware, combineReducers, compose, createStore, DeepPartial, Dispatch, Middleware,
Reducer, ReducersMapObject, StoreEnhancer
} from 'redux'

import { Action, ReducerBuilder, StateToProps } from '.'
import { Action, ReducerBuilder, StateToProps, Store } from './'

const devTool: StoreEnhancer = f =>
(window as any).__REDUX_DEVTOOLS_EXTENSION__ || f
Expand Down Expand Up @@ -143,26 +135,13 @@ export class StoreBuilder<S extends StoreState> {
return this
}

/**
* Dummy function to return `MapStateToProps` type that can be passed to `connect`
* As paramter, mapper function is required which takes store object and returns indexer object
* You can expose that function from your store object to be able to use on connected components.
* ex.
* const [store, mapStoreToProps] = new StoreBuilder<StoreState>().build()
* export { mapStoreToProps }
*
* @type {StateToProps<S>}
* @memberof StoreBuilder
*/
private mapStoreToProps: StateToProps<S> = map => map

/**
* Build an instance of store with configured values.
*
* @returns {Store<StoreType>}
* @memberof StoreBuilder
*/
public build(): [Store<S>, StateToProps<S>] {
public build(): Store<S> {
const defer = Promise.defer<Dispatch<Action>>()
const reducerMap = Object.keys(this.reducerBuilders).reduce(
(p: any, r) => ({
Expand All @@ -178,6 +157,13 @@ export class StoreBuilder<S extends StoreState> {

defer.resolve(store.dispatch)

return [store, this.mapStoreToProps]
return {
...store,
mapStoreToProps: map => map,
connected: (mapStateToProps, mapDispatchToProps) => mapDispatchToProps
? (connect(mapStateToProps, mapDispatchToProps) as any)
: (connect(mapStateToProps) as any)

}
}
}
6 changes: 3 additions & 3 deletions tests/reducer.builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('Reducer', () => {
isBasicActionCalled: true,
})

const [store] = new StoreBuilder<StoreState>()
const store = new StoreBuilder<StoreState>()
.withReducerBuildersMap({ reducer })
.build()

Expand All @@ -41,7 +41,7 @@ describe('Reducer', () => {
return state
})

const [store] = new StoreBuilder<StoreState>()
const store = new StoreBuilder<StoreState>()
.withReducerBuilder('reducer', reducer)
.build()

Expand Down Expand Up @@ -84,7 +84,7 @@ describe('Reducer', () => {
return next(a)
})
.withReducerBuildersMap({ reducer })
.build()[0]
.build()

store.dispatch(SimpleAction())
})
Expand Down
10 changes: 5 additions & 5 deletions tests/store.builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Store', () => {
const initState = { reducer: { test: true } }

describe('with initial state', () => {
const [store] = new StoreBuilder()
const store = new StoreBuilder()
.withInitialState(initState)
.withReducersMap({ reducer })
.build()
Expand All @@ -29,7 +29,7 @@ describe('Store', () => {
isSet = true
return next(action)
}
const [store] = new StoreBuilder()
const store = new StoreBuilder()
.withMiddleware(testMiddleware)
.withReducersMap({ reducer })
.build()
Expand All @@ -49,7 +49,7 @@ describe('Store', () => {
}
return state
}
const [store] = new StoreBuilder().withReducer('test', testReducer).build()
const store = new StoreBuilder().withReducer('test', testReducer).build()

store.dispatch(testAction)

Expand All @@ -66,7 +66,7 @@ describe('Store', () => {
}
return state
}
const [store] = new StoreBuilder().withReducersMap({ testReducer }).build()
const store = new StoreBuilder().withReducersMap({ testReducer }).build()

store.dispatch(testAction)

Expand All @@ -81,7 +81,7 @@ describe('Store', () => {
isSet = true
return f
}
const [store] = new StoreBuilder()
const store = new StoreBuilder()
.withReducersMap({ reducer })
.withEnhancer(enhancer)
.build()
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"outDir": "./lib",
"lib": ["dom", "es5", "es2015.promise", "es2015.core"]
},
"exclude": ["node_modules", "tests"]
"exclude": ["node_modules", "tests", "lib", "dist"]
}

0 comments on commit ed6d414

Please sign in to comment.