1.8.1 • Published 2 years ago

app-redux-utils v1.8.1

Weekly downloads
13
License
MIT
Repository
github
Last release
2 years ago

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

// User.store.ts
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.

// configRedux.ts
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({
    // register our reducers
    user: UserStore.reducer,
  });

  const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({
    // add cheap-di container to as DI resolver
    container,
  });

  const store = configureStore({
    reducer: rootReducer,
    middleware: (getDefaultMiddleware) =>
      // add our middleware to redux
      getDefaultMiddleware().concat([middleware]),
  });

  return store;
}
// State.ts
import { configureRedux } from "./configureRedux";

// infer type from reducers registration
export type State = ReturnType<ReturnType<typeof configureRedux>["getState"]>;

Create a controller to encapsulate a piece of application business logic.

// 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 // 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>) {
    this.dispatch(UsersActions.updateStore(partialStore));
  }

  @watch // add register action creator with name of method
  async loadUserList() {
    this.updateStore({
      usersAreLoading: true,
    });

    const response = await fetch('/api/users');
    if (!response.ok) {
      this.updateStore({
        usersAreLoading: false,
      });

      // show error notification or something else
      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.

// 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!

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 = 
  // 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 };

Register dependencies

// UserApi.ts
export class UserApi {
  loadUsers() {
    return fetch('/api/users');
  }
}
// App.tsx
import { useEffect } from 'react';
import { container } from 'cheap-di';
import { UserApi } from './UserApi';

const App = () => {
  useEffect(() => {
    container.registerType(UserApi);
  }, []);

  return /* your layout */;
}
// 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 };

Without decorators

If you can't use decorators, you have to add watcher to your controller.

// User.watcher.ts
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', // typescript will check that this string corresponds to real method name in your controller
    ],
    [
      UserActions.LOAD_USER,
      'loadUser',
    ],
    //...
  ]);
// controllerWatchers.ts
import { Watcher } from 'app-redux-utils';
import { State } from "./State";
import { UserWatcher } from '/User.watcher';

const controllerWatchers: Watcher<State>[] = [
  UserWatcher,
  // rest watchers
];

export { controllerWatchers };
// configRedux.ts
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;

// Users.actions.ts
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);
}
// UsersPageContainer.ts
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()(
      // this action will be dispatched after loadCurrentUser will be handled in controller
      () => UsersActions.loadSomethingElse()
    )
  ),
});

const UsersPageContainer: ComponentType = connect(
  null,
  mapDispatchToProps
)(UsersPage);

export { UsersPageContainer };

Using with redux-saga

// User.saga.ts
import { Action } from "app-redux-utils";

export class UsersSaga {
  static * loadUsers() {/* ... */}
  static * loadUser(action: Action<{ userID: string }>) {/* ... */}
}
// User.watcher.ts
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));
  }
}
// configureRedux.ts
export function configureRedux() {
  // ...
  const sagaMiddleware = /* create middleware*/;

  const watcher = new UserWatcher();
  watcher.run(sagaMiddleware);

  // ...
}
1.8.1

2 years ago

1.7.2

2 years ago

1.8.0

2 years ago

1.7.1

2 years ago

1.7.0

2 years ago

1.6.0

2 years ago

1.5.4

3 years ago

1.5.3

3 years ago

1.5.2

3 years ago

1.5.1

3 years ago

1.5.0

3 years ago

1.4.1

3 years ago

1.4.0

3 years ago

1.3.5

3 years ago

1.3.4

3 years ago

1.3.3

3 years ago

1.3.2

3 years ago

1.3.1

3 years ago

1.3.0

3 years ago

1.2.0

4 years ago

1.1.2

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago