- install
- [usage](#How to use controllers)
- [controllers-di](#With cheap-di)
- [reduce-boilerplate](#Reduce boilerplate)
- [controllers](#Using controllers)
- [redux-saga](#Using with redux-saga)
This library is not maintained anymore, please use https://github.com/TomasLight/react-redux-controller
Installation
npm install app-redux-utils
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
import { Reducer } from 'app-redux-utils';
class UserStore {
users: string[] = [];
usersAreLoading = false;
static update = 'USER_update_store';
static reducer = Reducer(new UserStore(), UserStore.update);
}
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.
import { combineReducers } from "redux";
import { configureStore } from "@reduxjs/toolkit";
import { controllerMiddleware } from "app-redux-utils";
import { container } from "cheap-di";
import { UserStore } from "./User.store";
export function configureRedux() {
const rootReducer = combineReducers({
user: UserStore.reducer,
});
const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({
container,
});
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat([middleware]),
});
return store;
}
import { configureRedux } from "./configureRedux";
export type State = ReturnType<ReturnType<typeof configureRedux>["getState"]>;
Create a controller to encapsulate a piece of application business logic.
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> {
private updateStore(partialStore: Partial<UsersStore>) {
this.dispatch(UsersActions.updateStore(partialStore));
}
@watch
async loadUserList() {
this.updateStore({
usersAreLoading: true,
});
const response = await fetch('/api/users');
if (!response.ok) {
this.updateStore({
usersAreLoading: false,
});
return;
}
const users = await response.json();
this.updateStore({
usersAreLoading: false,
users,
});
}
@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.
import { useDispatch } from 'react-redux';
import { UserController } from './UserController';
const UserList = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(UserController.loadUsers());
dispatch(UserController.loadUser({ userID: '123' }));
}, []);
return <>...</>;
};
That's it!
Custom action names
You can use custom action name, like @watch('myCustomActionName')
.
But in this case you should change define this method with DecoratedWatchedController
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 =
Omit<WatchedController<UserController>, 'loadCurrentUser'>
& DecoratedWatchedController<[
['loadChatInfo', { userID: string; }]
]>;
const typedController = (UserController as unknown) as Controller;
export { typedController as UserController };
Register dependencies
export class UserApi {
loadUsers() {
return fetch('/api/users');
}
}
import { useEffect } from 'react';
import { container } from 'cheap-di';
import { UserApi } from './UserApi';
const App = () => {
useEffect(() => {
container.registerType(UserApi);
}, []);
return ;
}
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 };
Without decorators
If you can't use decorators, you have to add watcher to your controller.
import { watcher } from 'app-redux-utils';
import { UserActions } from './User.actions';
import { UserController } from './User.controller';
export const UserWatcher = watcher<UserController>(
UserController,
[
[
UserActions.LOAD_USER_LIST,
'loadUserList',
],
[
UserActions.LOAD_USER,
'loadUser',
],
]);
import { Watcher } from 'app-redux-utils';
import { State } from "./State";
import { UserWatcher } from '/User.watcher';
const controllerWatchers: Watcher<State>[] = [
UserWatcher,
];
export { controllerWatchers };
import { combineReducers } from "redux";
import { controllerMiddleware } from "app-redux-utils";
import { controllerWatchers } from "./controllerWatchers";
export function configureRedux() {
const rootReducer = combineReducers();
const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({
container,
watchers: controllerWatchers,
});
return store;
}
Manual action creating
You can define action creators by yourself;
import { createAction, createActionWithCallback } from "app-redux-utils";
import { UsersStore } from "./Users.store";
export class UsersActions {
static readonly PREFIX = "USERS_";
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 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);
}
import { ComponentType } from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { UsersActions } from "./Users.actions";
import { Props, UsersPage } from "./UsersPage";
const mapDispatchToProps = (dispatch: Dispatch): Props => ({
loadUsers: () => dispatch(UsersActions.loadUserList()),
loadUser: (userID: string) => dispatch(UsersActions.loadUser({ userID })),
loadCurrentUser: () => dispatch(
UsersActions.loadCurrentUser()(
() => UsersActions.loadSomethingElse()
)
),
});
const UsersPageContainer: ComponentType = connect(
null,
mapDispatchToProps
)(UsersPage);
export { UsersPageContainer };
Using with redux-saga
import { Action } from "app-redux-utils";
export class UsersSaga {
static * loadUsers() {}
static * loadUser(action: Action<{ userID: string }>) {}
}
import { SagaMiddleware } from "redux-saga";
import { ForkEffect, put, PutEffect, TakeEffect, takeLatest } from "@redux-saga/core/effects";
import { Action } from "app-redux-utils";
import { UserActions } from "../redux/User.actions";
import { UserSaga } from "./User.saga";
type WatchFunction = () => IterableIterator<ForkEffect | TakeEffect | PutEffect>;
export class UserWatcher {
public watchFunctions: WatchFunction[];
constructor() {
this.watchFunctions = [];
this.watchLatest(
UserActions.LOAD_USERS,
UserSaga.loadUsers
);
this.watchLatest(
UserActions.LOAD_USER,
UserSaga.loadUser
);
}
private getSagaWithCallbackAction(saga: (action: Action) => void): (action: Action) => void {
return function* (action: Action) {
yield saga(action);
if (!action.stopPropagation) {
const actions = action.getActions();
const putActionEffects = actions.map(action => put(action()));
yield all(putActionEffects);
}
};
}
private watchLatest(actionType: string, saga: (action: Action) => void) {
const sagaWithCallbackAction = this.getSagaWithCallbackAction(saga);
this.watchFunctions.push(
function* () {
yield takeLatest(actionType, sagaWithCallbackAction);
}
);
}
public run(sagaMiddleware: SagaMiddleware) {
this.watchFunctions.forEach(saga => sagaMiddleware.run(saga));
}
}
export function configureRedux() {
const sagaMiddleware = ;
const watcher = new UserWatcher();
watcher.run(sagaMiddleware);
}