react-redux-annotation
TypeScript icon, indicating that this package has built-in type declarations

1.0.10 • Public • Published

react-redux-annotation

A plugin to easily configure your react / redux / typescript project using class and decorators. It actually work:

  • without middleware (redux sync)
  • with redux-thunk
  • with redux-saga

Soon will integrate redux-promise and redux-observable.

Why it makes your code cleaner? Because decorators manage for you:

  • reducer <-> action binding
  • react property <-> state binding
  • react method <-> action binding
  • more globally connection between react component and redux store

Install it using

npm i --save react-redux-annotation

See demo-* folder and run :

npm run watch
npm run start

You have to import one of these files:

import {   } from "react-redux-annotation/redux";
import {   } from "react-redux-annotation/thunk";
import {   } from "react-redux-annotation/saga";

Decorators

The plugin provide these decorators/functions:

  • @ReduxConnect: connect react component to the redux store
  • @BindAction: bind an action and a reducer
  • @DefaultAction: set a reducer as default if action is not recognized
  • @ConnectAction / @ConnectSaga: connect a react props methods to an action
  • @ConnectProp: connect a react props property to the global state
  • exportReducers: a function to export reducers

Using without middlware

Sample

Below a sample explaining how to use it with redux store.

State

export interface Counter {
  loading: boolean;
  value: number
}
export const initialState: Counter = {
    value: 0, loading: false
}

Actions

import * as redux from 'redux'
import * as state from './state'
 
//DEFINE A PAYLOAD INTERFACE USED ON REDUCER
export interface JumpPayload {
  value: number
}
export class Actions { 
  static JUMP = "JUMP";
  static ADD = "ADD"; 
  static INCREMENT = "INCREMENT"; 
  static RESET = "RESET";
  //ONLY DEFINE ACTION METHOD HAVING A PAYLOAD OR PARAMS OR LOGIC (API calls)
  static jump(  value: number) {
    return { type: Actions.JUMP, value };
  } 
} 

Reducers

import { combineReducers } from 'redux'
import { BindAction, exportReducers, DefaultAction } from "react-redux-annotation";
import { Actions, JumpPayload } from "./actions"
import { Counter } from "./state"
 
export class Reducers {
  @BindAction(Actions.JUMP)//YOU CAN USE ACTION PAYLOAD DEFINED IN THE ACTION FILE FOR STRING TYPING
  add(state: Counter, action: JumpPayload): Counter {
    return { ...state, value: state.value + action.value };
  } 
  @BindAction([Actions.INCREMENT, Actions.ADD])//YOU CAN BIND A REDUCER TO MULTIPLE ACTION
  incrementSuccess(state: Counter, action: any): Counter {
    return { value: state.value + 1, loading: false };
  }
  @BindAction(Actions.RESET)
  reset(state: Counter, action: any): Counter {
    return { ...state, value: 0 };
  }
  @DefaultAction()//BIND A DEFAULT REDUCER IF YOU WANT => by default it returns the current state
  initial(): Counter {
    return { value: 0, loading: false }
  }
}
//EXPORT REDUCERS - OPTIONS ARE NOT REQUIRED. useReference LET YOU COMPARE ACTION BY REFERENCE (enabling having duplicate). BY DEFAULT IT IS TRUE
export const cReducers = exportReducers<Counter>(Reducers,{useReference:false});

React component

import * as React from 'react'
import * as redux from 'redux'
import { connect } from 'react-redux'
import { Actions } from './actions'
import * as state from './state'
import { ConnectAction, ConnectProp, ReduxConnect } from "react-redux-annotation/redux";
 
 
class Props {
  myName?= "Nabil"; //DEFINE DEFAULT PARAMS OR ANY OTHER PARAMS NOT CONNECTED TO THE STORE
  @ConnectAction(Actions.INCREMENT) increment?: () => void //CONNECT THE PROPS METHOD TO AN ACTION TYPE
  @ConnectAction(Actions.RESET) reset?: () => void;
  @ConnectAction(Actions.jump) jump?: (value: number) => void;//CONNECT THE PROPS METHOD TO AN ACTION FUNCTION. YOU CAN DEFINE PARAMS OR IMPORT PAYLOAD FROM ACTION FILE
  @ConnectProp((sta: state.Counter) => sta) counter?: state.Counter;//CONNECT A PROPS PROPERTY TO THE STATE USING A SELECTOR
}
 
@ReduxConnect(Props) //CONNECT THE COMPONENT TO THE REDUX STORE
export class PureCounter extends React.Component<Props> {
  _onClickJump = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.jump(4) }
  _onClickIncrement = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.increment() }
  _onClickReset = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.reset() }
  render() {
    const { counter } = this.props
    return <div>
      <div >
        <strong>{counter.value} - {this.props.myName}</strong><br />
        {this.props.counter.loading && <h6>Loading...</h6>}
      </div>
      <form>
        <button onClick={this._onClickIncrement}>Increment</button>
        <button onClick={this._onClickReset}>Reset</button>
        <button onClick={this._onClickJump}>Jump 4</button> 
      </form>
    </div>
  }
} 

Configure the store

import * as React from 'react'
import * as ReactDOM from 'react-dom'
import * as redux from 'redux'
import { Provider } from 'react-redux'
import * as reducers from './reducers'
import * as state from './state'
import { PureCounter } from './counter'
 
const store: redux.Store<state.Counter> = redux.createStore(
  reducers.cReducers,
  state.initialState
)
const Root: React.SFC<{}> = () => (
  <Provider store={store}>
    <PureCounter />
  </Provider>
)
window.addEventListener('DOMContentLoaded', () => {
  const rootEl = document.getElementById('redux-app-root')
  ReactDOM.render(<Root />, rootEl)
})
 

Redux thunk

The configuration with redux-thunk is more or less the same. Only action and action binding changed.

Actions

The decorator automatically send the dispatch function as first parameter:

import * as redux from 'redux'
import * as state from './state'
 
//DEFINE A PAYLOAD INTERFACE
export interface JumpPayload {
  value: number
}
export class Actions { 
  static JUMP = "JUMP";
  static DECREMENT = "DECREMENT";
  static INCREMENT = "INCREMENT";
  static INCREMENT_SUCCESS = "INCREMENT_SUCCESS";
  static RESET = "RESET";  
  decrement(dispatch: redux.Dispatch<state.Counter>) {
    dispatch({ type: Actions.DECREMENT }); 
  }
  //ASYNC ACTION
  increment(dispatch: redux.Dispatch<state.Counter>) {
    dispatch({ type: Actions.INCREMENT });
    setTimeout(() => {
      dispatch({ type: Actions.INCREMENT_SUCCESS });
    }, 3000)
  }
  //ACTION WITH PARAM
  jump(dispatch: redux.Dispatch<state.Counter>, value: number) {
    dispatch({ type: Actions.JUMP, value });
  }
  reset(dispatch: redux.Dispatch<state.Counter>) {
    dispatch({ type: Actions.RESET });
  }
}
export const rActions = new Actions();

React component

You bind react props to an action using the reference to the function:

import * as React from 'react'
import * as redux from 'redux'
import { connect } from 'react-redux'
import { Actions } from './actions'
import * as state from './state' 
import { ConnectAction, ConnectProp, ReduxConnect } from "react-redux-annotation/thunk";
 
class Props {
  myName?= "Nabil";
  @ConnectAction(Actions.add) add?: () => void
  @ConnectAction(Actions.increment) increment?: () => void
  @ConnectAction(Actions.reset) reset?: () => void;
  @ConnectAction(Actions.jump) jump?: (value: number) => void;
  @ConnectProp((sta: state.Counter) => sta) counter?: state.Counter;
}
@ReduxConnect(Props)
export class PureCounter extends React.Component<Props> {
  _onClickJump = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.jump(4) }
  _onClickIncrement = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.increment() }
  _onClickReset = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.reset() }
  render() {
    const { counter } = this.props
    return <div>
      <div >
        <strong>{counter.value} - {this.props.myName}</strong><br />
        {this.props.counter.loading && <h6>Loading...</h6>}
      </div>
      <form>
        <button onClick={this._onClickIncrement}>Increment</button>
        <button onClick={this._onClickReset}>Reset</button>
        <button onClick={this._onClickJump}>Jump 4</button> 
      </form>
    </div>
  }
} 

Store

The configuration of the store looks like:

import thunk from 'redux-thunk'
import * as reducers from './reducers'
import * as state from './state' 
 
const store: redux.Store<state.Counter> = redux.createStore(
  reducers.cReducers,
  state.initialState,
  redux.applyMiddleware(thunk),
)
...

Redux Saga

The configuration with redux-thunk is more or less the same. Now you connect a method to a saga using @ConnectSaga

Define Saga

import * as redux from 'redux'
import * as state from './state'
import { delay } from 'redux-saga'
import { put, take, fork, cancel, cancelled, race, call } from 'redux-saga/effects'
// USE THE INTERFACE ON THE REACT COMPONENT
export interface PlayPayload {
  count: number
}
//USE THIS INTERFACE IN THE REDUCER (as params)
export interface ResetPayload {
  value: number;
  type: string;
}
export class Sagas {
  static START = "START";//DEFINE THE DISPATCH ACTIONS
  static DECREMENT = "DECREMENT";//USE IT ON REDUCER TO BIND THE REDUCER
  static PLAY = "PLAY";
  static STOP = "STOP";
  static PAUSE = "PAUSE";
  static RESUME = "RESUME";
  //USE THE PAYLOAD INTERFACE
  static * animate(payload: PlayPayload) { 
    yield delay(1000) 
    while (payload.count--) {
      yield put({ type: Sagas.DECREMENT }); 
      yield delay(750)
    } 
    yield put({ type: Sagas.STOP });
  }
  static * pause(payload: PlayPayload) {
    const { play } = yield race({
      stop: take(Sagas.STOP),
      play: take(Sagas.PLAY)
    })
    if (play) {
      yield call(Sagas.play, payload)
    }
  }
  static * play(payload: PlayPayload) { 
    const task = yield fork(Sagas.animate, payload); 
    const { pause } = yield race({
      stop: take(Sagas.STOP),
      pause: take(Sagas.PAUSE)
    })
    yield cancel(task)
    if (pause) {
      yield call(Sagas.pause, payload)
    }
  }
  //THE ENTRY POINT
  static * playFlow() {
    while (true) {
      const payload : PlayPayload= yield take(Sagas.PLAY)//GET THE PAYLOAD
      yield put({ type: Sagas.START, value: payload.count }); 
      yield call(Sagas.play, payload)
    }
  }
} 

React component

Connect the react props to the saga action / state properties:

import * as React from 'react'
import * as redux from 'redux'
import { connect } from 'react-redux'
import { PlayPayload, Sagas } from './saga'
import * as state from './state'
import { ConnectProp, ConnectSaga, ReduxConnect } from "react-redux-annotation/saga";
 
 
 
class Props {
  myName?= "Nabil";
  @ConnectSaga(Sagas.PLAY) play?: (payload: PlayPayload) => void //USE PAYLOAD INTERFACE
  @ConnectSaga(Sagas.PAUSE) pause?: () => void //CONNECT THE SAGA ACTION TO BE DISPATCHED
  @ConnectSaga(Sagas.STOP) stop?: () => void //USE CLEAN METHOD TO DISPATCH SAGA ACTION
  @ConnectProp((sta: state.Countdown) => {  return sta; }) counter?: state.Countdown;//BIND TO THE STATE USING SELECTOR
}
 
@ReduxConnect(Props)
export class PureCounter extends React.Component<Props> {
  _onPlay = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.play({ count: 10 })  }
  _onPause = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.pause() }
  _onStop = (e: React.SyntheticEvent<HTMLButtonElement>) => { this.props.stop() }
  render() {
    const { counter } = this.props
    return <div>
      <div >
        <strong>{counter.value} - {this.props.myName}</strong><br />
        {this.props.counter.playing == "play" && < h6 > Playing...</h6>}
      </div>
      <form>
        {this.props.counter.playing != "play" && <button onClick={this._onPlay}>Play</button>}
        {this.props.counter.playing == "play" && <button onClick={this._onPause}>Pause</button>}
        <button onClick={this._onStop}>Stop</button> 
      </form>
    </div >
  }
} 

Credits

Credits to https://rjz.github.io/typescript-react-redux/ for the demo-* folder. Demos are based on this example because it is clean and simple to understand.

TODO

  • add middlewares: promise, observable
  • add react-navigation
  • add type checking

Package Sidebar

Install

npm i react-redux-annotation

Weekly Downloads

1

Version

1.0.10

License

MIT

Unpacked Size

71.9 kB

Total Files

15

Last publish

Collaborators

  • nabil_mansouri