Typescript decorators\annotations, utils and abstract classes for react-redux application.
Adds various utility annotations such as @Single, @EntryPoint, @Connect or @Wrapped.
Allows you to write class-based declarations of your data storage with strict and predictive typing.
Enforces typesafety and OOP mixed with functional style (all key features and implementation of redux remains the same).
Intended to be used with react-redux.
Installation
npm install --save redux-cbd
Important:
- Package uses proposal ES reflect-metadata api, so I would advice to get acknowledged with its usage.
- Package uses 'expirementalDecorators' features (disabled by default for TypeScript transpiler).
Setup
1) Install package.
2) Inject 'reflect-metadata' into your bundle (webpack entry or import inside your entryfile).
3) Configure typescript. You should turn on "emitDecoratorMetadata" and "experimentalDecorators" for compiler.
4) Create some actions (extend simple, complex, async) with @ActionWired annotation.
5) Create related reducer(extend ReflectiveReducer) with proper @ActionHandlers.
6) Create rootReducer that includes reflectiveReducers. Declare storeState interface.
7) Create store based on root reducer. Extend CBDStoreManager, annotate @StoreManaged. Include cbdMiddleware there.
8) Create @StoreConnect decorator.
9) Connect component => use props and actions from declarative storage.
tsconfig.json part:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
What is inside
Example (wiki contains more explanations):
Application entrypoint:
import * as React from "react";
import {render} from "react-dom";
import {EntryPoint} from "redux-cbd";
import {GlobalStoreProvider} from "./data/redux";
import {ConnectedComponent, IConnectedComponentExternalProps} from "./view/ConnectedComponent";
@EntryPoint
export class Application {
public static main(): void {
render( <GlobalStoreProvider>
<ConnectedComponent someLabelFromExternalProps={ "Demo prop" } { ...{} as IConnectedComponentExternalProps }/>
</GlobalStoreProvider>, document.getElementById("application-root"));
}
}
Store, provider and connect creations:
import {GlobalStoreManager} from "./GlobalStoreManager";
import {IGlobalStoreState} from "./IGlobalStoreState";
export {IGlobalStoreState} from "./IGlobalStoreState";
export const globalStoreManager: GlobalStoreManager = new GlobalStoreManager();
export const GlobalStoreProvider = globalStoreManager.getProviderComponent();
export const GlobalStoreConnect = globalStoreManager.getConsumerAnnotation();
State declarations:
export class DemoReducerState {
public storedNumber: number = 0;
public loading: boolean = false;
}
export interface IGlobalStoreState {
demoReducer: DemoReducerState;
}
Our demo reducer:
import {ActionHandler, ReflectiveReducer} from "redux-cbd";
import {AsyncDemoAction, AsyncDemoActionSuccess, ComplexDemoAction, SimpleDemoAction, DataExchangeDemoAction} from "../actions";
import {DemoReducerState} from "../state/DemoReducerState";
export class DemoReducer extends ReflectiveReducer<DemoReducerState> {
@ActionHandler()
public changeStoredNumber(state: DemoState, action: SimpleDemoAction): DemoState {
return { ...state, storedNumber: action.payload.storedNumber };
}
@ActionHandler()
public exchangeSomeData(state: DemoState, action: DataExchangeDemoAction): DemoState {
return { ...state, storedNumber: action.payload.storedNumber };
}
@ActionHandler()
public startLoadingOnAsyncActionReceived(state: DemoState, action: AsyncDemoAction): DemoState {
return { ...state, loading: action.payload.loading };
}
@ActionHandler()
public finishFakeLoading(state: DemoState, action: AsyncDemoActionSuccess): DemoState {
return { ...state, storedNumber: action.payload.storedNumber, loading: false };
}
@ActionHandler()
public handleComplexAction(state: DemoState, action: ComplexDemoAction): DemoState {
return { ...state, storedNumber: action.payload.storedNumber };
}
}
Our actions for reducer methods (considered to be separate class-files, you know):
import {ActionWired, AsyncAction, SimpleAction, DataExchangeAction} from "redux-cbd";
@ActionWired("DATA_EXCHANGE_TEST_ACTION")
export class DataExchangeDemoAction extends DataExchangeAction<{ storedNumber: number }> {}
@ActionWired("SIMPLE_TEST_ACTION")
export class SimpleDemoAction extends SimpleAction {
public payload: { storedNumber: number } = { storedNumber: 0 };
public constructor(num: number) {
super();
this.payload.storedNumber = num;
}
}
@ActionWired("ASYNC_TEST_ACTION_SUCCESS")
export class AsyncDemoActionSuccess extends SimpleAction {
public payload: { loading: boolean, storedNumber: number } = { loading: true, storedNumber: -1 };
public constructor(num: number) {
super();
this.payload.storedNumber = num;
}
}
@ActionWired("ASYNC_TEST_ACTION")
export class AsyncDemoAction<DemoState> extends AsyncAction {
public payload: { loading: boolean } = { loading: true };
private readonly delay: number;
public constructor(delay: number) {
super();
this.payload.loading = true;
this.delay = delay;
}
public async act(): Promise<number> {
const forMillis = (delay: number) => new Promise(resolve => setTimeout(resolve, delay));
await forMillis(this.delay);
return Math.random();
}
public afterSuccess(num: number): AsyncDemoActionSuccess {
return new AsyncDemoActionSuccess(num);
}
}
@ActionWired("COMPLEX_TEST_ACTION")
export class ComplexDemoAction<DemoState> extends ComplexAction {
public payload: { storedNumber: number } = { storedNumber: 0 };
public constructor(num: number) {
super();
this.payload.storedNumber = num;
}
public act(): void {
this.payload.storedNumber *= 1000 + 500 * Math.random();
}
}
Global store manager:
import {Action, combineReducers, Store, applyMiddleware, createStore, Middleware, Reducer} from "redux";
import {StoreManaged, CBDStoreManager, cbdMiddleware} from "redux-cbd";
import {logInConnectedComponentMiddleware, logInConsoleMiddleware} from "../../view/logInMiddlewares";
import {IGlobalStoreState} from "./IGlobalStoreState";
import {DemoReducerState} from "../demo/state/DemoReducerState";
import {DemoReducer} from "../demo/reducer/DemoReducer";
@StoreManaged("GLOBAL_STORE")
export class GlobalStoreManager extends CBDStoreManager<IGlobalStoreState> {
protected createStore(): Store<IGlobalStoreState, Action<any>> {
const middlewares: Array<Middleware> = [cbdMiddleware, logInConnectedComponentMiddleware, logInConsoleMiddleware];
return createStore(this.createRootReducer(), applyMiddleware(...middlewares));
}
private createRootReducer(): Reducer<IGlobalStoreState> {
return combineReducers( {
demoReducer: new DemoReducer().asFunctional(new DemoReducerState(), { freezeState: true })
});
}
}
Connected component
import * as React from "react";
import {PureComponent} from "react";
import {Action} from "redux";
import {Bind} from "redux-cbd";
import {GlobalStoreConnect, IGlobalStoreState} from "../data";
import {AsyncDemoAction, SimpleDemoAction, ComplexDemoAction, DataExchangeDemoAction} from "../data/demo/actions";
interface IConnectedComponentStoreProps {
demoLoading: boolean;
demoNumber: number;
}
interface IConnectedComponentDispatchProps {
sendSimpleDemoAction: (num: number) => SimpleDemoAction;
sendAsyncDemoAction: (num: number) => AsyncDemoAction;
sendComplexDemoAction: (num: number) => ComplexDemoAction;
sendDataExchangeDemoAction: (num: number) => DataExchangeDemoAction;
}
export interface IConnectedComponentOwnProps {
someLabelFromExternalProps: string;
}
export interface IConnectedComponentExternalProps extends IConnectedComponentStoreProps,
IConnectedComponentDispatchProps {}
export interface IConnectedComponentProps extends IConnectedComponentOwnProps, IConnectedComponentExternalProps {}
@GlobalStoreConnect<IConnectedComponentStoreProps, IConnectedComponentDispatchProps, IConnectedComponentProps>(
(store: IGlobalStoreState) => {
return {
demoLoading: store.demoReducer.loading,
demoNumber: store.demoReducer.storedNumber
};
}, {
sendSimpleDemoAction: (num: number) => new SimpleDemoAction(num),
sendComplexDemoAction: (num: number) => new ComplexDemoAction(num),
sendAsyncDemoAction: (num: number) => new AsyncDemoAction(num),
sendDataExchangeDemoAction: (num) => new DataExchangeDemoAction({ storedNumber: num })
})
export class ConnectedComponent extends PureComponent<IConnectedComponentProps> {
public static actionsLog: Array<Action> = [];
public renderLogMessages(): JSX.Element[] {
return ConnectedComponent.actionsLog.map((item, idx) => <div key={idx}> {JSON.stringify(item)} </div>);
}
public render(): JSX.Element {
const {someLabelFromExternalProps, demoLoading, demoNumber} = this.props;
const paddingStyle = { padding: "10px" };
return (
<div style={paddingStyle}>
<div> Also, check console. External prop: [{ someLabelFromExternalProps }]: </div>
<div style={paddingStyle}>
<b>Demo Reducer:</b> <br/> <br/>
[testLoading]: {demoLoading.toString()} ; <br/>
[testValue]: {demoNumber.toString()} ; <br/>
</div>
<br/>
<div style={paddingStyle}>
<button onClick={this.sendSimpleDemoAction}>Send Sync Action</button>
<button onClick={this.sendDataExchangeAction}>Send Data Exchange Action</button>
<button onClick={this.sendAsyncAction}>Send Async Action</button>
<button onClick={this.sendComplexAction}>Send Complex Action</button>
<button onClick={this.clearLogMessages}>Clean</button>
</div>
<div>
<div>Actions log:</div>
{this.renderLogMessages()}
</div>
</div>
);
}
@Bind
public clearLogMessages(): void {
ConnectedComponent.actionsLog = [];
this.forceUpdate();
}
@Bind
private sendSimpleDemoAction(): void {
this.props.sendSimpleDemoAction(Math.random() * 999 + 1);
}
@Bind
private sendDataExchangeAction(): void {
this.props.sendDataExchangeDemoAction(Math.random() * 9999 + 1000)
}
@Bind
private sendComplexAction(): void {
this.props.sendComplexDemoAction(Math.random() * -9999 - 1)
}
@Bind
private sendAsyncAction(): void {
this.props.sendComplexDemoAction(Math.random() * -99999 - 10000)
}
}
Documentation:
Repository wiki includes doc and samples.
Proposals and contribution:
Feel free to contibute or mail me with questions/proposals/issues (Neloreck@gmail.com).
Full examples
Repository includes example project with commentaries: link.
My own 'redux-cbd' based project: link.
Library unit tests also include some different examples of cbd usage: link .
Licence
MIT