1.4.1 • Published 1 year ago

@tomas_light/react-redux-controller v1.4.1

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Installation

npm install @tomas_light/react-redux-controller

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 '@tomas_light/react-redux-controller';

class UserStore {
  users: string[] = [];
  usersAreLoading = false;

  static update = 'USER_update_store';
  static reducer = Reducer(new UserStore(), UserStore.update);
}

Register store reducer and add react-redux-controller 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 "@tomas_light/react-redux-controller";
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>>({
    // use cheap-di container for Dependency Injection
    container,
  });

  const store = configureStore({
    reducer: rootReducer,
    middleware: (getDefaultMiddleware) =>
      // add react-redux-controller 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 logic.

// User.controller.ts
import { ControllerBase, createAction, watch } from '@tomas_light/react-redux-controller';
import { State } from "./State";
import { UsersStore } from "./Users.store";

// prepare the class to use static methods for creating of actions
@watch
export class UserController extends ControllerBase<State> {
  // just to shorcat store update calls
  private updateStore(partialStore: Partial<UsersStore>) {
    this.dispatch(createAction(UsersStore.update, partialStore));
  }

  // add action creator with name of the method: { type: 'loadUserList' }
  @watch
  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 loadProfile(action: Action<{ userID: string }>) {/*...*/}
  @watch loadSomethingElse() {/*...*/}
}

// there are restrictions from decorators in TS - it cannot to change type of the decorated class,
// so we should manually do it =(
// WatchedController takes all methods of the class and adds type definition for static action creators
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.loadProfile({ userID: '123' }));
  }, []);
  
  return <>...</>;
};

That's it!

Custom action names

You can use custom action name, like @watch('myCustomActionName'). In this case you should to pass name mapper type as second argument of WatchedController

import { ControllerBase, watch, WatchedController } from '@tomas_light/react-redux-controller';
import { State } from "./State";

@watch
export class UserController extends ControllerBase<State> {
  /* ... */

  @watch loadUser(action: Action<{ userID: string }>) {/* ... */}
  @watch('loadChatById') loadChatByIdFromSpecialStorage(action: Action<{ chatId: string }>) {/* ... */}
}

const typedController = (UserController as unknown) as WatchedController<UserController, {
  loadChatByIdFromSpecialStorage: 'loadChatById', // map original method name to the new one
}>;
export { typedController as UserController };

Register dependencies

// api.ts

const metadata = <T>(constructor: T): T => constructor;

export class UserApi {
  loadUsers() {
    return fetch('/api/users');
  }
}

export abstract class AccessKey {
  abstract key: string;
}

@metadata // need any decorator (read cheap-di docs)
export class UserStorage {
  constructor(private readonly accessKey: AccessKey) {}
  
  store(value: string) {
    localStorage.set(this.accessKey.key, value);
  }
}
// App.tsx
import { useEffect } from 'react';
import { container } from 'cheap-di';
import { AccessKey } from './api';

const App = () => {
  useEffect(() => {
    container.registerInstance({ key: 'my-secure-key' }).as(AccessKey);
  }, []);

  return /* your layout */;
}
// User.controller.ts
import { ControllerBase, watch } from '@tomas_light/react-redux-controller';
import { State } from "./State";
import { UserApi, UserStorage } from './api';

@watch
export class UserController extends ControllerBase<State> {
  constructor(
    middleware: Middleware<State>,
    private readonly api: UserApi, // will be instantiated automaticly
    private readonly storage: UserStorage // will be instantiated with registered AccessKey
  ) {
    super(middleware);
  }

  @watch
  async loadUserList() {
    const response = await this.api.loadUsers();
    this.storage.store(response.data);
    // ...
  }
}

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 '@tomas_light/react-redux-controller';
import { UserActions } from './User.actions';
import { UserController } from './User.controller';

export const UserWatcher = watcher(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 '@tomas_light/react-redux-controller';
import { UserWatcher } from '/User.watcher';

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

export { controllerWatchers };
// configRedux.ts
import { combineReducers } from "redux";
import { controllerMiddleware } from "@tomas_light/react-redux-controller";
import { controllerWatchers } from "./controllerWatchers";

export function configureRedux() {
  const rootReducer = combineReducers(/*...*/);

  const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({
    container,
    watchers: controllerWatchers,
  });

  // ...

  return store;
}

Action creating

You can define action creators by yourself;

import { createAction } from "@tomas_light/react-redux-controller";

export class UsersActions {
  static LOAD_USER_LIST = 'LOAD_USER_LIST';
  static loadUserList = () => createAction(UsersActions.LOAD_USER_LIST);

  static LOAD_USER = 'LOAD_USER';
  static loadUser = (data: { userID: string }) => createAction(UsersActions.LOAD_USER, data);
}
1.4.1

1 year ago

1.4.0

1 year ago

1.2.0

1 year ago

1.3.0

1 year ago

1.1.0

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago