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 '
export interface JumpPayload {
value : number
}
export class Actions {
static JUMP = " JUMP " ;
static ADD = " ADD " ;
static INCREMENT = " INCREMENT " ;
static RESET = " RESET " ;
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 )
add ( state : Counter , action : JumpPayload ) : Counter {
return { ... state , value : state . value + action . value } ;
}
@ BindAction ( [ Actions . INCREMENT , Actions . ADD ] )
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 ( )
initial ( ) : Counter {
return { value : 0 , loading : false }
}
}
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 " ;
@ 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 >
}
}
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 '
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 } ) ;
}
increment ( dispatch : redux . Dispatch < state . Counter > ) {
dispatch ( { type : Actions . INCREMENT } ) ;
setTimeout ( ( ) => {
dispatch ( { type : Actions . INCREMENT_SUCCESS } ) ;
} , 3000 )
}
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 '
export interface PlayPayload {
count : number
}
export interface ResetPayload {
value : number ;
type : string ;
}
export class Sagas {
static START = " START " ;
static DECREMENT = " DECREMENT " ;
static PLAY = " PLAY " ;
static STOP = " STOP " ;
static PAUSE = " PAUSE " ;
static RESUME = " RESUME " ;
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 )
}
}
static * playFlow ( ) {
while ( true ) {
const payload : PlayPayload = yield take ( Sagas . PLAY )
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
@ ConnectSaga ( Sagas . PAUSE ) pause ? : ( ) => void
@ ConnectSaga ( Sagas . STOP ) stop ? : ( ) => void
@ ConnectProp ( ( sta : state . Countdown ) => { return sta ; } ) counter ? : state . Countdown ;
}
@ 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