app-redux-utils
Advanced tools
Comparing version 1.7.0 to 1.7.1
"use strict"; | ||
var ControllerBase_1; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ControllerBase = void 0; | ||
class ControllerBase { | ||
const tslib_1 = require("tslib"); | ||
const Middleware_1 = require("../Middleware"); | ||
// simple decorator allows Reflect-metadata to scan classes ang get its metadata | ||
// it is needed to auto resolve Middleware dependency | ||
const metadata = (constructor) => constructor; | ||
let ControllerBase = ControllerBase_1 = class ControllerBase { | ||
constructor(middleware) { | ||
if (new.target === ControllerBase) { | ||
if (new.target === ControllerBase_1) { | ||
throw new Error('Cannot construct ControllerBase instance directly'); | ||
@@ -12,4 +18,8 @@ } | ||
} | ||
} | ||
}; | ||
ControllerBase = ControllerBase_1 = tslib_1.__decorate([ | ||
metadata, | ||
tslib_1.__metadata("design:paramtypes", [Middleware_1.Middleware]) | ||
], ControllerBase); | ||
exports.ControllerBase = ControllerBase; | ||
//# sourceMappingURL=ControllerBase.js.map |
@@ -5,3 +5,2 @@ export * from './Middleware'; | ||
export * from './createActionWithCallback'; | ||
export * from './createReducers'; | ||
export { Action, ActionWithCallback, CallbackAction, isAction, DecoratedWatchedController, WatchedController, } from './types'; | ||
@@ -8,0 +7,0 @@ export * from './Reducer'; |
@@ -9,3 +9,2 @@ "use strict"; | ||
tslib_1.__exportStar(require("./createActionWithCallback"), exports); | ||
tslib_1.__exportStar(require("./createReducers"), exports); | ||
var types_1 = require("./types"); | ||
@@ -12,0 +11,0 @@ Object.defineProperty(exports, "isAction", { enumerable: true, get: function () { return types_1.isAction; } }); |
{ | ||
"name": "app-redux-utils", | ||
"version": "1.7.0", | ||
"version": "1.7.1", | ||
"description": "Helpful utils for redux", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
486
README.md
* [install](#Installation) | ||
* [usage](#How to use) | ||
* [usage](#How to use controllers) | ||
* [controllers-di](#With cheap-di) | ||
* [reduce-boilerplate](#Reduce boilerplate) | ||
* [controllers](#Using controllers) | ||
* [controllers-di](#Using controllers with cheap-di) | ||
* [reduce-boilerplate](#Reduce boilerplate) | ||
* [redux-saga](#Using with redux-saga) | ||
@@ -11,82 +11,58 @@ | ||
```bush | ||
```bash | ||
npm install app-redux-utils | ||
``` | ||
### <a name="usage"></a> How to use | ||
### <a name="usage"></a> How to use controllers | ||
Controller - is place for piece of logic in your application. | ||
The differences from Saga (in `redux-saga`) is your methods is not static! | ||
It allows you to use dependency injection technics and simplify tests in some cases. | ||
Create your store | ||
```ts | ||
// User.store.ts | ||
import { User } from './User'; | ||
import { Reducer } from 'app-redux-utils'; | ||
export class UserStore { | ||
users: User[]; | ||
usersAreLoading: boolean; | ||
} | ||
``` | ||
class UserStore { | ||
users: string[] = []; | ||
usersAreLoading = false; | ||
```ts | ||
// Users.actions.ts | ||
import { createAction, createActionWithCallback } from "app-redux-utils"; | ||
import { UsersStore } from "./Users.store"; | ||
export interface LoadUserData { | ||
userId: number; | ||
static update = 'USER_update_store'; | ||
static reducer = Reducer(new UserStore(), UserStore.update); | ||
} | ||
export class UsersActions { | ||
static readonly PREFIX = "USERS_"; | ||
static readonly UPDATE_STORE = `${UsersActions.PREFIX}UPDATE_STORE`; | ||
static readonly LOAD_USER_LIST = `${UsersActions.PREFIX}LOAD_USER_LIST`; | ||
static readonly LOAD_USER = `${UsersActions.PREFIX}LOAD_USER`; | ||
static readonly LOAD_CURRENT_USER = `${UsersActions.PREFIX}LOAD_CURRENT_USER`; | ||
static readonly LOAD_SOMETHING_ELSE = `${UsersActions.PREFIX}LOAD_SOMETHING_ELSE`; | ||
static updateStore = (partialStore: Partial<UsersStore>) => | ||
createAction(UsersActions.UPDATE_STORE, partialStore); | ||
static loadUserList = () => createAction(UsersActions.LOAD_USER_LIST); | ||
static loadUser = (data: LoadUserData) => createAction(UsersActions.LOAD_USER, data); | ||
static loadCurrentUser = () => createActionWithCallback(UsersActions.LOAD_CURRENT_USER); | ||
static loadSomethingElse = () => createAction(UsersActions.LOAD_SOMETHING_ELSE); | ||
} | ||
``` | ||
Register store reducer and add app-redux-utils middleware to redux. | ||
You can also register DI container, that allows you to inject services in controllers. | ||
```ts | ||
// UsersPageContainer.ts | ||
import { ComponentType } from "react"; | ||
import { connect } from "react-redux"; | ||
import { Dispatch } from "redux"; | ||
// configRedux.ts | ||
import { combineReducers } from "redux"; | ||
import { configureStore } from "@reduxjs/toolkit"; | ||
import { UsersActions } from "./Users.actions"; | ||
import { Props, UsersPage } from "./UsersPage"; | ||
import { controllerMiddleware } from "app-redux-utils"; | ||
import { container } from "cheap-di"; | ||
const mapDispatchToProps = (dispatch: Dispatch): Props => ({ | ||
loadUserList: () => dispatch(UsersActions.loadUserList()), | ||
loadUser: (userId: number) => dispatch(UsersActions.loadUser({ userId })), | ||
loadCurrentUser: () => dispatch( | ||
UsersActions.loadCurrentUser()( | ||
// this action will be dispatched after loadCurrentUser will be handled in controller | ||
() => UsersActions.loadSomethingElse() | ||
) | ||
), | ||
}); | ||
import { UserStore } from "./User.store"; | ||
const UsersPageContainer: ComponentType = connect( | ||
null, | ||
mapDispatchToProps | ||
)(UsersPage); | ||
export function configureRedux() { | ||
const rootReducer = combineReducers({ | ||
// register our reducers | ||
user: UserStore.reducer, | ||
}); | ||
export { UsersPageContainer }; | ||
``` | ||
const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({ | ||
// add cheap-di container to as DI resolver | ||
container, | ||
}); | ||
```ts | ||
// Users.reducer.ts | ||
import { Reducer } from "app-redux-utils"; | ||
const store = configureStore({ | ||
reducer: rootReducer, | ||
middleware: (getDefaultMiddleware) => | ||
// add our middleware to redux | ||
getDefaultMiddleware().concat([middleware]), | ||
}); | ||
import { UsersActions } from "./Users.actions"; | ||
import { UsersStore } from "./Users.store"; | ||
export const UsersReducer = Reducer(new UsersStore(), UsersActions.UPDATE_STORE); | ||
return store; | ||
} | ||
``` | ||
@@ -96,56 +72,12 @@ | ||
// State.ts | ||
import { UsersStore } from "./Users.store"; | ||
import { SomeStore } from "./Some.store"; | ||
import { configureRedux } from "./configureRedux"; | ||
export class State { | ||
public usersStore: UsersStore; | ||
public someStore: SomeStore; | ||
} | ||
// infer type from reducers registration | ||
export type State = ReturnType<ReturnType<typeof configureRedux>["getState"]>; | ||
``` | ||
Create a controller to encapsulate a piece of application business logic. | ||
```ts | ||
// getReducers.ts | ||
import { ReducersMapObject } from "redux"; | ||
import { State } from "./State"; | ||
import { UsersReducer } from "./Users.reducer"; | ||
import { SomeReducer } from "./Some.reducer"; | ||
export function getReducers(): ReducersMapObject<State, any> { | ||
return { | ||
usersStore: UsersReducer, | ||
someStore: SomeReducer, | ||
}; | ||
} | ||
``` | ||
```ts | ||
// configureApp.ts | ||
import { createReducers } from "app-redux-utils"; | ||
import { applyMiddleware, compose, createStore, Store } from "redux"; | ||
import { State } from "./State"; | ||
import { getReducers } from "./getReducers"; | ||
export function configureApp(): Store<State> { | ||
const devtoolsComposer = window["__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"]; | ||
const composeEnhancer = devtoolsComposer || compose; | ||
const store: Store<State> = createStore( | ||
createReducers(getReducers), | ||
composeEnhancer(applyMiddleware()) | ||
); | ||
return store; | ||
} | ||
``` | ||
### <a name="controllers"></a> Using controllers | ||
Controller - is place for piece of logic in your application. | ||
It differences from Saga in redux-saga - your methods is not static! | ||
It allows you to use dependency injection technics and simplify tests in some cases. | ||
```ts | ||
// User.controller.ts | ||
import { ControllerBase } from 'app-redux-utils'; | ||
import { ControllerBase, watch } from 'app-redux-utils'; | ||
import { State } from "./State"; | ||
@@ -155,3 +87,5 @@ import { UsersActions } from "./Users.actions"; | ||
@watch // this decorator adds action creators as static members of class | ||
export class UserController extends ControllerBase<State> { | ||
// just to shorcat store update calls | ||
private updateStore(partialStore: Partial<UsersStore>) { | ||
@@ -161,2 +95,3 @@ this.dispatch(UsersActions.updateStore(partialStore)); | ||
@watch // add register action creator with name of method | ||
async loadUserList() { | ||
@@ -185,11 +120,120 @@ this.updateStore({ | ||
loadUser(action: Action<LoadUserData>) {/* ... */} | ||
loadCurrentUser() {/* ... */} | ||
loadSomethingElse() {/* ... */} | ||
@watch loadUser(action: Action<{ userID: string }>) {/*...*/} | ||
@watch loadCurrentUser() {/*...*/} | ||
@watch loadSomethingElse() {/*...*/} | ||
} | ||
const typedController = (UserController as unknown) as WatchedController<UserController>; | ||
export { typedController as UserController }; | ||
``` | ||
And now you can dispatch the controller actions from a component. | ||
```tsx | ||
// UserList.ts | ||
import { useDispatch } from 'react-redux'; | ||
import { UserController } from './UserController'; | ||
const UserList = () => { | ||
const dispatch = useDispatch(); | ||
useEffect(() => { | ||
// create action and dispatch it in one line | ||
dispatch(UserController.loadUsers()); | ||
dispatch(UserController.loadUser({ userID: '123' })); | ||
}, []); | ||
return <>...</>; | ||
}; | ||
``` | ||
That's it! | ||
#### <a name="usage"></a> Custom action names | ||
You can use custom action name, like `@watch('myCustomActionName')`. | ||
But in this case you should change define this method with `DecoratedWatchedController` | ||
```ts | ||
import { ControllerBase, watch, DecoratedWatchedController } from 'app-redux-utils'; | ||
import { State } from "./State"; | ||
@watch | ||
export class UserController extends ControllerBase<State> { | ||
/* ... */ | ||
@watch loadUser(action: Action<{ userID: string }>) {/* ... */} | ||
@watch('loadChatInfo') loadCurrentUser(action: Action<{ chat: boolean }>) {/* ... */} | ||
} | ||
type Controller = | ||
// keep infered type for all actions except action with custom action type | ||
Omit<WatchedController<UserController>, 'loadCurrentUser'> | ||
// specify type for custom action | ||
& DecoratedWatchedController<[ | ||
['loadChatInfo', { userID: string; }] | ||
]>; | ||
const typedController = (UserController as unknown) as Controller; | ||
export { typedController as UserController }; | ||
``` | ||
### <a name="controller-di"></a> Register dependencies | ||
```ts | ||
// UserApi.ts | ||
export class UserApi { | ||
loadUsers() { | ||
return fetch('/api/users'); | ||
} | ||
} | ||
``` | ||
```ts | ||
// App.tsx | ||
import { useEffect } from 'react'; | ||
import { container } from 'cheap-di'; | ||
import { UserApi } from './UserApi'; | ||
const App = () => { | ||
useEffect(() => { | ||
container.registerType(UserApi); | ||
}, []); | ||
return /* your layout */; | ||
} | ||
``` | ||
```ts | ||
// User.controller.ts | ||
import { ControllerBase, watch } from 'app-redux-utils'; | ||
import { State } from "./State"; | ||
import { UsersActions } from "./Users.actions"; | ||
import { UsersStore } from "./Users.store"; | ||
@watch | ||
export class UserController extends ControllerBase<State> { | ||
constructor(middleware: Middleware<State>, private userApi: UserApi) { | ||
super(middleware); | ||
} | ||
@watch | ||
async loadUserList() { | ||
const response = await this.userApi.loadUsers(); | ||
/*...*/ | ||
} | ||
} | ||
const typedController = (UserController as unknown) as WatchedController<UserController>; | ||
export { typedController as UserController }; | ||
``` | ||
### <a name="usage"></a> Without decorators | ||
If you can't use decorators, you have to add watcher to your controller. | ||
```ts | ||
// User.watcher.ts | ||
import { watcher } from 'app-saga-utils'; | ||
import { watcher } from 'app-redux-utils'; | ||
@@ -216,3 +260,3 @@ import { UserActions } from './User.actions'; | ||
// controllerWatchers.ts | ||
import { Watcher } from 'app-saga-utilsr'; | ||
import { Watcher } from 'app-redux-utils'; | ||
import { State } from "./State"; | ||
@@ -228,18 +272,19 @@ import { UserWatcher } from '/User.watcher'; | ||
``` | ||
```ts | ||
// configureApp.ts | ||
// configRedux.ts | ||
import { combineReducers } from "redux"; | ||
import { controllerMiddleware } from "app-redux-utils"; | ||
import { createStore, /* ... */ } from "redux"; | ||
// ... | ||
import { controllerWatchers } from "./controllerWatchers"; | ||
// ... | ||
export function configureApp(): Store<State> { | ||
export function configureRedux() { | ||
const rootReducer = combineReducers(/*...*/); | ||
const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({ | ||
container, | ||
watchers: controllerWatchers, | ||
}); | ||
// ... | ||
const store: Store<State> = createStore( | ||
// ... | ||
controllerMiddleware(controllerWatchers) // here we connect middleware | ||
); | ||
return store; | ||
@@ -249,114 +294,55 @@ } | ||
### <a name="controller-di"></a> Using controllers with cheap-di | ||
```ts | ||
// configureApp.ts | ||
import { controllerMiddleware } from "app-redux-utils"; | ||
import { container } from 'cheap-di'; | ||
import { createStore, /* ... */ } from "redux"; | ||
import { controllerWatchers } from "./controllerWatchers"; | ||
// ... | ||
export function configureApp(): Store<State> { | ||
// ... | ||
const store: Store<State> = createStore( | ||
// ... | ||
controllerMiddleware(controllerWatchers, container) // add container as second param | ||
); | ||
return store; | ||
} | ||
``` | ||
### <a name="controller-di"></a> Manual action creating | ||
You can define action creators by yourself; | ||
```ts | ||
// User.controller.ts | ||
import { ControllerBase, AbstractStore } from 'app-redux-utils'; | ||
import { State } from "./State"; | ||
import { UsersActions } from "./Users.actions"; | ||
// Users.actions.ts | ||
import { createAction, createActionWithCallback } from "app-redux-utils"; | ||
import { UsersStore } from "./Users.store"; | ||
import { metadata } from "./metadata"; // read cheap-di README to know what is it | ||
@metadata | ||
export class UserController extends ControllerBase<State> { | ||
constructor( | ||
store: AbstractStore<State>, | ||
private readonly service: MyService | ||
) { | ||
super(store); | ||
} | ||
// ... | ||
} | ||
``` | ||
export class UsersActions { | ||
static readonly PREFIX = "USERS_"; | ||
```ts | ||
// App.tsx | ||
import { useEffect } from 'react'; | ||
import { container } from 'cheap-di'; | ||
static readonly LOAD_USER_LIST = `${UsersActions.PREFIX}LOAD_USER_LIST`; | ||
static readonly LOAD_USER = `${UsersActions.PREFIX}LOAD_USER`; | ||
static readonly LOAD_CURRENT_USER = `${UsersActions.PREFIX}LOAD_CURRENT_USER`; | ||
static readonly LOAD_SOMETHING_ELSE = `${UsersActions.PREFIX}LOAD_SOMETHING_ELSE`; | ||
const App = () => { | ||
useEffect(() => { | ||
container.registerType(MyService); | ||
}, []); | ||
return /* your layout */; | ||
static loadUserList = () => createAction(UsersActions.LOAD_USER_LIST); | ||
static loadUser = (data: { userID: string }) => createAction(UsersActions.LOAD_USER, data); | ||
static loadCurrentUser = () => createActionWithCallback(UsersActions.LOAD_CURRENT_USER); | ||
static loadSomethingElse = () => createAction(UsersActions.LOAD_SOMETHING_ELSE); | ||
} | ||
``` | ||
### <a name="reduce-boilerplate"></a> Reduce boilerplate | ||
```ts | ||
// UsersPageContainer.ts | ||
import { ComponentType } from "react"; | ||
import { connect } from "react-redux"; | ||
import { Dispatch } from "redux"; | ||
You can avoid boilerplate by using decorators | ||
import { UsersActions } from "./Users.actions"; | ||
import { Props, UsersPage } from "./UsersPage"; | ||
`MyController.ts` | ||
```tsx | ||
import { ControllerBase, DecoratedWatchedController, Reducer, createAction, WatchedController } from 'app-redux-utils'; | ||
import { useDispatch } from 'react-redux'; | ||
const mapDispatchToProps = (dispatch: Dispatch): Props => ({ | ||
loadUsers: () => dispatch(UsersActions.loadUserList()), | ||
loadUser: (userID: string) => dispatch(UsersActions.loadUser({ userID })), | ||
loadCurrentUser: () => dispatch( | ||
UsersActions.loadCurrentUser()( | ||
// this action will be dispatched after loadCurrentUser will be handled in controller | ||
() => UsersActions.loadSomethingElse() | ||
) | ||
), | ||
}); | ||
type State = { | ||
my: MyStore; | ||
}; | ||
const UsersPageContainer: ComponentType = connect( | ||
null, | ||
mapDispatchToProps | ||
)(UsersPage); | ||
class MyStore { | ||
users: string[] = []; | ||
static update = 'My_update_store'; | ||
static reducer = Reducer(new MyStore(), MyStore.update); | ||
} | ||
@watch | ||
class MyController extends ControllerBase<State> { | ||
updateStore(store: Partial<MyStore>) { | ||
this.dispatch(createAction(MyStore.update, store)); | ||
} | ||
@watch('openUserForEditing') | ||
openUser(action: Action<{ userID: string; }>) { | ||
//... | ||
} | ||
} | ||
const myController: DecoratedWatchedController<[ | ||
'loadUsers' | | ||
['openUserForEditing', { userID: string; }] | ||
]> = MyController as any; | ||
// if you use the same name for all Controller methods (like `@watch addUser(action: ...) {...}`) | ||
// const myController: WatchedController<MyController> = MyController as any; | ||
export { myController as MyController }; | ||
export { UsersPageContainer }; | ||
``` | ||
```tsx | ||
import { useDispatch } from 'react-redux'; | ||
import { MyController } from './MyController'; | ||
const MyComponent = () => { | ||
const dispatch = useDispatch(); | ||
dispatch(MyController.loadUsers()); | ||
dispatch(MyController.openUserForEditing({ userID: '123' })); | ||
return <>beatiful button</>; | ||
}; | ||
``` | ||
@@ -366,11 +352,8 @@ ### <a name="redux-saga"></a> Using with redux-saga | ||
```ts | ||
// Users.saga.ts | ||
// User.saga.ts | ||
import { Action } from "app-redux-utils"; | ||
import { LoadUserData } from "../redux/Users.actions"; | ||
export class UsersSaga { | ||
static * loadUserList() {/* ... */} | ||
static * loadUser(action: Action<LoadUserData>) {/* ... */} | ||
static * loadCurrentUser() {/* ... */} | ||
static * loadSomethingElse() {/* ... */} | ||
static * loadUsers() {/* ... */} | ||
static * loadUser(action: Action<{ userID: string }>) {/* ... */} | ||
} | ||
@@ -380,4 +363,3 @@ ``` | ||
```ts | ||
// Users.watcher.ts | ||
// User.watcher.ts | ||
import { SagaMiddleware } from "redux-saga"; | ||
@@ -387,8 +369,8 @@ import { ForkEffect, put, PutEffect, TakeEffect, takeLatest } from "@redux-saga/core/effects"; | ||
import { UsersActions } from "../redux/Users.actions"; | ||
import { UsersSaga } from "./Users.saga"; | ||
import { UserActions } from "../redux/User.actions"; | ||
import { UserSaga } from "./User.saga"; | ||
type WatchFunction = () => IterableIterator<ForkEffect | TakeEffect | PutEffect>; | ||
export class UsersWatcher { | ||
export class UserWatcher { | ||
public watchFunctions: WatchFunction[]; | ||
@@ -400,13 +382,9 @@ | ||
this.watchLatest( | ||
UsersActions.LOAD_USERS, | ||
UsersSaga.loadUsers | ||
UserActions.LOAD_USERS, | ||
UserSaga.loadUsers | ||
); | ||
this.watchLatest( | ||
UsersActions.LOAD_USER, | ||
UsersSaga.loadUser | ||
UserActions.LOAD_USER, | ||
UserSaga.loadUser | ||
); | ||
this.watchLatest( | ||
UsersActions.LOAD_CURRENT_USER, | ||
UsersSaga.loadCurrentUser | ||
); | ||
} | ||
@@ -442,24 +420,12 @@ | ||
```ts | ||
// configureApp.ts | ||
import { createReducers } from "app-redux-utils"; | ||
import { applyMiddleware, createStore /* ... */ } from "redux"; | ||
import createSagaMiddleware, { SagaMiddleware } from "redux-saga"; | ||
import { State } from "./State"; | ||
import { getReducers } from "./getReducers"; | ||
import { UsersWatcher } from "./Users.watcher"; | ||
export function configureApp(): Store<State> { | ||
// configureRedux.ts | ||
export function configureRedux() { | ||
// ... | ||
const sagaMiddleware = /* create middleware*/; | ||
const store: Store<State> = createStore( | ||
createReducers(getReducers), | ||
composeEnhancer(applyMiddleware(createSagaMiddleware())) | ||
); | ||
const watcher = new UsersWatcher(); | ||
const watcher = new UserWatcher(); | ||
watcher.run(sagaMiddleware); | ||
return store; | ||
// ... | ||
} | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
49405
551
419