Nearly Perpendicular Macaroons

    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

    Install

    npm i react-redux-annotation

    DownloadsWeekly Downloads

    2

    Version

    1.0.10

    License

    MIT

    Unpacked Size

    71.9 kB

    Total Files

    15

    Last publish

    Collaborators

    • nabil_mansouri