Powerful extensitions for building class-based Redux architecture powered by TypeScript.
Example of using redux-typed-kit-example
-
Run
npm i redux-typed-kit
-
Configure your Babel to support class properties and decorators inside
.babelrc
"plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose": true }] ]
-
If you use TypeScript you should configure it inside
tsconfig.json
"experimentalDecorators": true, "emitDecoratorMetadata": true,
-
If you use React you also should install reflect-metadata
npm i reflect-metadata
and then import it at the top of the entry file
import 'reflect-metadata';
Let's create a simple Redux-architecture to fetch users from server and display them. We'll also take care about possible network (and others) errors.
Let's describe users state
import { State } from 'redux-typed-kit';
export default class UsersState extends State {
public users: Array<User> = [];
public filter: UsersFilter = new UsersFilter();
public isLoading: boolean = false;
public error: ApiError;
}
AppState should include all other states
import { State } from 'redux-typed-kit';
export default class AppState extends State {
public usersState: UsersState;
}
The State
class has the Rebuild
method, which is needed to create an instance of the State
class, based on the parent instance, but with some changes. It's very helpful to use in any reducers, especially when you need to save the type of your state class.
state.rebuild(x => {
x.isLoading = true;
});
Let's create three actions for fetching users. Action to start fetching, action in case of success and in case of error.
import { Action } from 'redux-typed-kit';
export class FetchUsersAction extends Action {
constructor(public filter: FetchUsersFilter) {
super();
}
}
export class FetchUsersSuccessAction extends Action {
constructor(public response: FetchUsersResponse) {
super();
}
}
export class FetchUsersFailureAction extends Action {
constructor(public error: ApiError) {
super();
}
}
Let's create users reducer. Every reducer must have two basic things: initial state and name. This name will be used to configure the global app state.
import { Reducer, ActionDecorator as Action } from 'redux-typed-kit';
import UsersState from '../models/states/users-state';
import { FetchUsersAction, FetchUsersSuccessAction, FetchUsersFailureAction } from '../actions/fetch-users-action';
export default class UsersReducer extends Reducer<UsersState> {
initialState = new UsersState();
name = 'usersState';
@Action
fetchUsers(state: UsersState, action: FetchUsersAction): UsersState {
return state.rebuild(x => {
x.isLoading = !action.isBackgroud;
});
}
@Action
fetchUsersSuccess(state: UsersState, action: FetchUsersSuccessAction): UsersState {
return state.rebuild(x => {
x.users = action.response.users;
x.isLoading = false;
});
}
@Action
fetchUsersFailure(state: UsersState, action: FetchUsersFailureAction): UsersState {
return state.rebuild(x => {
x.error = action.error;
x.isLoading = false;
});
}
}
CombinedReducer
combines several reducers into one complex reducer and state object just like combineReducers
function does.
import { CombinedReducer } from 'redux-typed-kit';
export default new CombinedReducer('complexState', new SimpleReducer(), new AnotherSimpleReducer());
RootReducer
combines others reducers (including CombinedReducer
) and used by store.
import { RootReducer } from 'redux-typed-kit';
import UsersReducer from './users-reducer';
const rootReducer = new RootReducer(
new UsersReducer() /*, new CombinedReducer(...), new AnotherReducer()*/
);
export default rootReducer;
We will use middleware to make a request to the server, process the response, and send data to the reducers.
import { PostMiddlewareHandler, Middleware, Store } from 'redux-typed-kit';
import AppState from '../models/states/app-state';
import { FetchUsersAction, FetchUsersSuccessAction, FetchUsersFailureAction, FetchUsersCancelAction } from '../actions/fetch-users-action';
export default class UsersMiddleware extends Middleware<AppState> {
@PostMiddlewareHandler
async fetchUsers(store: Store<AppState>, action: FetchUsersAction) {
const result = await api.fetchUsers(action.filter);
if (result.error == null) {
store.dispatch(new FetchUsersSuccessAction(result.response));
} else {
store.dispatch(new FetchUsersFailureAction(result.error));
}
}
}
PreMiddlewareHandler
decorator is used when we need to catch any dispached actions before they get to the reducers. So we can get state from Store
before any changes are applied inside the reducers.
PostMiddlewareHandler
decorator works exactly the opposite. All reducers already handled the action and changed the state.
Probably the best example I can give you is middlware for logging all actions and state's changes.
import { PostMiddlewareHandler, PreMiddlewareHandler, Middleware, Store } from 'redux-typed-kit';
export default class LoggerMiddleware extends Middleware<AppState> {
@PreMiddlewareHandler
preLog(store: Store<AppState>, action: any) {
console.log('PRE', action, JSON.parse(JSON.stringify(store.getState())));
}
@PostMiddlewareHandler
postLog(store: Store<AppState>, action: any) {
console.log('POST', action, JSON.parse(JSON.stringify(store.getState())));
}
}
Store
is a combination of RootReducer
and Middlewares
that create a final state object.
import { Store } from 'vue-redux-ts';
import AppState from '../models/states/app-state';
import rootReducer from '../reducers/root-reducer';
import UsersMiddleware from '../middlewares/users-middleware';
const store = new Store<AppState>(rootReducer, new UsersMiddleware());
export default store.init();