wooline / react-coat

Structured React + Redux with Typescript and support for isomorphic rendering beautifully(SSR)
MIT License
285 stars 12 forks source link
mobx react redux server-side-rendering ssr state-management typescript

Build Status TypeScript MIT Licence

I'm not going to maintain this framework anymore, because there's a better alternative, That is another project for me and can be seen as the next version of the this framework:

Welcome to Medux

This package is already outdated! Welcome to medux

English | 简体中文

The opening, freedom and prosperity of react ecosphere also lead to tedious development and configuration and confused choice.Reaction-coat abandons some flexibility, replaces some configurations with conventions, solidifies some best practices, and provides developers with a more concise sugar coat.

Are you still honestly maintaining the store according to the native Redux tutorial?Try react-coat, which is so simple that you can do it almost without learning.

For example:

//  Only one class, action、reducer、effect、loading
class ModuleHandlers extends BaseModuleHandlers {
  @reducer
  protected putCurUser(curUser: CurUser): State {
    return {...this.state, curUser};
  }
  @reducer
  public putShowLoginPop(showLoginPop: boolean): State {
    return {...this.state, showLoginPop};
  }
  @effect("login") // use loading state
  public async login(payload: {username: string; password: string}) {
    const loginResult = await sessionService.api.login(payload);
    if (!loginResult.error) {
      // this.updateState() is a shortcut to this.dispatch(this.actions.updateState())
      this.updateState({curUser: loginResult.data});
      Toast.success("welcome!");
    } else {
      Toast.fail(loginResult.error.message);
    }
  }
  // uncatched error will dispatch @@framework/ERROR action
  // subscribe it and reporting to the server
  @effect(null) // set null that means loading state are not needed
  protected async ["@@framework/ERROR"](error: CustomError) {
    if (error.code === "401") {
      // dispatch action: putShowLoginPop
      this.dispatch(this.actions.putShowLoginPop(true));
    } else if (error.code === "301" || error.code === "302") {
      // dispatch action: router change
      this.dispatch(this.routerActions.replace(error.detail));
    } else {
      Toast.fail(error.message);
      await settingsService.api.reportError(error);
    }
  }
  // subscribe itself's INIT Action and to do any async request
  @effect()
  protected async ["app/INIT"]() {
    const [projectConfig, curUser] = await Promise.all([
      settingsService.api.getSettings(),
      sessionService.api.getCurUser()
    ]);
    // this.updateState() is a shortcut to this.dispatch(this.actions.updateState())
    this.updateState({
      projectConfig,
      curUser,
    });
  }
}

Static checking and intelligent prompts with Typescript:

TS Types


4.0 Released

Feature

why not dvaJS

The framework is similar to dvajs in concept, and the main differences are as follows::

Install

$ npm install react-coat

peerDependencies

  "peerDependencies": {
    "@types/node": "^9.0.0 || ^10.0.0  || ^11.0.0",
    "@types/history": "^4.0.0",
    "@types/react": "^16.0.0",
    "@types/react-dom": "^16.0.0",
    "@types/react-redux": "^5.0.0 || ^6.0.0 || ^7.0.0",
    "@types/react-router-dom": "^4.0.0",
    "connected-react-router": "^5.0.0 || ^6.0.0",
    "history": "^4.0.0",
    "react": "^16.3.0",
    "react-dom": "^16.3.0",
    "react-redux": "^5.0.0 || ^6.0.0",
    "react-router-dom": "^4.0.0",
    "redux": "^3.0.0 || ^4.0.0"
  }

If you want to save your mind and have no special requirements for the dependent versions, you can install the react-coat-pkg of "all in 1", which will automatically contain the above libraries and the versions pass without conflict after test.

$ npm install react-coat-pkg

Compatibility

Mainstream browser、>=IE9 (with es6 polyfill,recommend @babel/polyfill)

List of API

Click to view


BaseModuleHandlers, BaseModuleState, buildApp, delayPromise, effect, ERROR, errorAction, exportModel, exportModule, exportView, GetModule, INIT, LoadingState, loadModel, loadView, LOCATION_CHANGE, logger, ModelStore, Module, ModuleGetter, reducer, renderApp, RootState, RouterParser, setLoading, setLoadingDepthTime

Quick Start and Demo

The framework is simple to use.

Basic concepts and nouns

Premise: Suppose you are familiar with React and Redux and have some development experience.

Store、Reducer、Action、State、Dispatch

The above concepts are basically the same as Redux. The framework is non-intrusive and follows the concepts and principles of react and redux:

Effect

We know that in Redux, changing the state must trigger the reducer through dispatch action and return a new State in the reducer. The reducer is a pure function with no side effects. As long as the input parameters are the same, the return results are the same and are executed synchronously.And effect is relative to reducer. Like reducer, it must also be triggered by dispatch action. The difference is:

ActionHandler

We can simply think that:In Redux, store.dispatch(action) can trigger a registered reducer, which seems to be an observer mode. Extending to the above concept of effect, effect is also an observer.An action is dispatched, which may trigger multiple observers to be executed. They may be reducer or effect. So reducer and effect are collectively called: ActionHandler

Module

When we receive a complex front-end project, we first need to simplify the complexity and disassemble the functions.It is usually divided into Modules according to the principles of high cohesion and low coupling. A module is a collection of relatively independent business functions. It usually includes a Model ( for processing business logic ) and a group of View ( for render data and interaction ). It should be noted that:

Module is a logical division, but we are used to using folder directories to organize and reflect, for example:

src
├── modules
│       ├── user
│       │     ├── userOverview(Module)
│       │     ├── userTransaction(Module)
│       │     └── blacklist(Module)
│       ├── agent
│       │     ├── agentOverview(Module)
│       │     ├── agentBonus(Module)
│       │     └── agentSale(Module)
│       └── app(Module)

As can be seen from the above, the project includes seven modules: app、userOverview、userTransaction、blacklist、agentOverview、agentBonus、agentSale, Although there are subdirectories user and angent under the modules directory, they are only classified and do not belong to modules. We agree that:

ModuleState、RootState

The system is divided into several relatively independent and level Modules, not only in the folder directory, but also in Store State. Each Module is responsible for maintaining and managing a node under the Store, which we call ModuleState, while the entire Store is customarily called RootState.

For example:A Store data structure:


{
router:{...},// StoreReducer
app:{...}, // ModuleState
userOverview:{...}, // ModuleState
userTransaction:{...}, // ModuleState
blacklist:{...}, // ModuleState
agentOverview:{...}, // ModuleState
agentBonus:{...}, // ModuleState
agentSale:{...} // ModuleState
}

You may notice that the first of the Store's sub-nodes above is router, which is not a ModuleState, but a node generated by a third-party Reducer.We know that Redux allows multiple Reducers to co-maintain Stroe and provides a combineReducers method for merging. Because the key name of ModuleState is Module name, so:Module names naturally cannot be renamed with other third-party Reducers.

Model

Within Module, we can further divide it into a model (maintenance data) and a set of views (render data). Here, the model actually refers to the view model, which mainly contains two functions:

Data flow flows from Model into View in one direction, so Model is independent and independent of View.So in theory, even without View, the program can still be driven from the command line.

We agree that:

For example, Model in the userOverview module:

src
├── modules
│       ├── user
│       │     ├── userOverview(Module)
│       │     │         ├──views
│       │     │         └──model.ts
│       │     │

src/modules/user/userOverview/model.ts

// Define ModuleState types
export interface State extends BaseModuleState {
  listSearch: {username:string; page:number; pageSize:number};
  listItems: {uid:string; username:string; age:number}[];
  listSummary: {page:number; pageSize:number; total:number};
  loading: {
    searchLoading: LoadingState;
  };
}

// Coding Module's ActionHandler
class ModuleHandlers extends BaseModuleHandlers<State, RootState, ModuleNames> {
  constructor() {
    // Define ModuleState's initial value
    const initState: State = {
      listSearch: {username:null, page:1, pageSize:20},
      listItems: null,
      listSummary: null,
      loading: {
        searchLoading: LoadingState.Stop,
      },
    };
    super(initState);
  }

  // Define a reducer
  @reducer
  public putSearchList({listItems, listSummary}): State {
    return {...this.state, listItems, listSummary};
  }

  // Define a effect that request data with ajax
  // And then dispatch a action that tigger putSearchList reducer
  // this.dispatch is store.dispatch's feference
  // searchLoading indicates to inject the execution state of this effect into State.loading.searchLoading
  @effect("searchLoading")
  public async searchList(options: {username?:string; page?:number; pageSize?:number} = {}) {
    // this.state is own ModuleState
    const listSearch = {...this.state.listSearch, ...options};
    const {listItems, listSummary} = await api.searchList(listSearch);
    this.dispatch(this.action.putSearchList({listItems, listSummary}));
  }

  // Define a effect that subscribed another module's action and then update its own ModuleState
  // Use protected permission because there is no need to actively call
  // @effect(null) indicates that there is no need to track the execution state
  @effect(null)
  protected async ["@@router/LOCATION_CHANGE]() {
      // this.rootState is the entire store state
      if(this.rootState.router.location.pathname === "/list"){
          await this.dispatch(this.action.searchList());
      }
  }
}

In particular, the last ActionHandler of the above code:

protected async ["@@router/LOCATION_CHANGE](){
    if(this.rootState.router.location.pathname === "/list"){
        await this.dispatch(this.action.searchList());
    }
}

Two points have been emphasized before:

Also note the statement:await this.dispatch(this.action.searchList()):

View、Component

Within the Module, we can further divide it into a model ( maintenance data ) and a group of view ( render data ).So there may be more than one view in a Module, and we are used to creating a folder named views under the Module root directory:

For example, views in the userOverview module:

src
├── modules
│       ├── user
│       │     ├── userOverview(Module)
│       │     │         ├──views
│       │     │         │     ├──imgs
│       │     │         │     ├──List
│       │     │         │     │     ├──index.css
│       │     │         │     │     └──index.ts
│       │     │         │     ├──Main
│       │     │         │     │    ├──index.css
│       │     │         │     │    └──index.ts
│       │     │         │     └──index.ts
│       │     │         │
│       │     │         │
│       │     │         └──model.ts
│       │     │

For example: LoginForm:

interface Props extends DispatchProp {
  logining: boolean;
}

class Component extends React.PureComponent<Props> {
  public onLogin = (evt: any) => {
    evt.stopPropagation();
    evt.preventDefault();
    // ActionHandler in Model is triggered by dispatch action
    this.props.dispatch(thisModule.actions.login({username: "", password: ""}));
  };

  public render() {
    const {logining} = this.props;
    return (
      <form className="app-Login" onSubmit={this.onLogin}>
        <h3>Login</h3>
        <ul>
          <li><input name="username" placeholder="Username" /></li>
          <li><input name="password" type="password" placeholder="Password" /></li>
          <li><input type="submit" value="Login" disabled={logining} /></li>
        </ul>
      </form>
    );
  }
}

const mapStateToProps = (state: RootState) => {
  return {
    logining: state.app.loading.login !== LoadingState.Stop,
  };
};

export default connect(mapStateToProps)(Component);

As you can see from the above code, View is a Component. Is there any difference between View and Component? No coding, logically there are:

Routing and Dynamic Loading

React-coat agrees with the idea of react-router 4 modular routing. Routing is a component. Nested routing is as simple as nested component, without complicated configuration. Such as: PhotosView and VideosView come from Photos module and Videos module respectively. They are loaded asynchronously on demand.

import {BottomNav} from "modules/navs/views"; // BottomNav come from another module
import LoginForm from "./LoginForm"; // LoginForm come from this module

// PhotosView an VideosView come from other module
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
const VideosView = loadView(moduleGetter, ModuleNames.videos, "Main");

<div className="g-page">
    <Switch>
        <Route exact={false} path="/photos" component={PhotosView} />
        <Route exact={false} path="/videos" component={VideosView} />
        <Route exact={true} path="/login" component={LoginForm} />
    </Switch>
    <BottomNav />
</div>

Several other views are nested in one of the above views in different loading modes:

Therefore, the framework is flexible and simple to load modules and views without complex configuration and modification.

Several special actions

Roadmap

with the API unchanged, React Hooks will be used to replace Redux and React-Redux to facilitate user's senseless upgrade.

with the same API, Mobx will be used to replace Redux.