Skip to content

Commit b6959a0

Browse files
committed
Release 1.5.0: renamed Reducer to createReducer
* update readme * renamed package to redux-controller-middleware
1 parent cb9f1c4 commit b6959a0

File tree

15 files changed

+407
-165
lines changed

15 files changed

+407
-165
lines changed

.github/workflows/release-package.yml

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,5 @@ jobs:
1414
node-version: 16
1515
- run: npm ci
1616
- run: npm test
17-
18-
publish-gpr:
19-
needs: build
20-
runs-on: ubuntu-latest
21-
permissions:
22-
packages: write
23-
contents: read
24-
steps:
25-
- uses: actions/checkout@v3
26-
- uses: actions/setup-node@v3
27-
with:
28-
node-version: 16
29-
registry-url: https://npm.pkg.github.com/
30-
- run: npm ci
31-
- run: npm publish
32-
env:
33-
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
17+
- name: Upload coverage reports to Codecov
18+
uses: codecov/codecov-action@v3

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/node_modules/
22
.idea/**
33
/dist/
4-
.yarn
4+
.yarn
5+
/test-coverage

.npmrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

README.md

Lines changed: 130 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,76 @@
1-
* [Installation](#install)
2-
* [How to use controllers](#usage)
3-
* [Register dependencies](#controller-di)
4-
* [Without decorators](#without-decorators)
5-
* [Action creating](#action-creating)
6-
7-
### <a name="install"></a> Installation
8-
9-
```bash
10-
npm install @tomas_light/react-redux-controller
1+
# redux-controller-middleware
2+
3+
Adjust Redux middleware to be able to use controllers with Dependency Injection to handle actions.
4+
5+
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mui/material-ui/blob/HEAD/LICENSE)
6+
[![npm latest package](https://img.shields.io/npm/v/redux-controller-middleware/latest.svg)](https://img.shields.io/npm/v/redux-controller-middleware/latest.svg)
7+
[![codecov](https://codecov.io/github/tomas-light/redux-controller-middleware/branch/main/graph/badge.svg?token=NuAoioGPVD)](https://codecov.io/github/redux-controller-middleware)
8+
9+
* [Installation](#installation)
10+
* [How to use](#how-to-use)
11+
* [Custom action names](#custom-action-names)
12+
* [Dependency injection](#dependency-injection)
13+
* [createAction](#createAction)
14+
* [Chaining actions](#chaining-action)
15+
* [Without decorators](#without-decorators)
16+
17+
## <a name="installation"></a> Installation
18+
npm
19+
```cmd
20+
npm install redux-controller-middleware
21+
```
22+
yarn
23+
```cmd
24+
yarn add redux-controller-middleware
1125
```
1226

13-
### <a name="usage"></a> How to use controllers
27+
## <a name="how-to-use"></a> How to use
1428

15-
Controller - is place for piece of logic in your application.
29+
Controller - is a place for a piece of logic in your application.
1630
The differences from Saga (in `redux-saga`) is your methods is not static!
17-
It allows you to use dependency injection technics and simplify tests in some cases.
31+
It allows you to use dependency injection technics and simplify tests.
1832

1933
Create your store
2034
```ts
2135
// User.store.ts
22-
import { Reducer } from '@tomas_light/react-redux-controller';
36+
import { createReducer } from 'redux-controller-middleware';
2337

2438
class UserStore {
2539
users: string[] = [];
2640
usersAreLoading = false;
2741

2842
static update = 'USER_update_store';
29-
static reducer = Reducer(new UserStore(), UserStore.update);
43+
static reducer = createReducer(new UserStore(), UserStore.update);
3044
}
3145
```
3246

3347
Register store reducer and add react-redux-controller middleware to redux.
3448
You can also register DI container, that allows you to inject services in controllers.
3549
```ts
36-
// configRedux.ts
37-
import { combineReducers } from "redux";
38-
import { configureStore } from "@reduxjs/toolkit";
39-
import { controllerMiddleware } from "@tomas_light/react-redux-controller";
40-
import { container } from "cheap-di";
50+
// configureRedux.ts
51+
import { configureStore } from '@reduxjs/toolkit';
52+
import { container } from 'cheap-di';
53+
import { combineReducers } from 'redux';
54+
import { controllerMiddleware } from 'redux-controller-middleware';
4155

42-
import { UserStore } from "./User.store";
56+
import { UserStore } from './User.store';
4357

4458
export function configureRedux() {
4559
const rootReducer = combineReducers({
4660
// register our reducers
4761
user: UserStore.reducer,
4862
});
63+
64+
// infer type from reducers registration
65+
type ConfiguredState = ReturnType<typeof rootReducer>;
66+
type InferredState = {
67+
[storeName in keyof ConfiguredState as storeName extends
68+
| '[unknown]'
69+
| symbol
70+
| number
71+
? never
72+
: storeName]: ConfiguredState[storeName];
73+
};
4974

5075
const middleware = controllerMiddleware<ReturnType<typeof rootReducer>>({
5176
// use cheap-di container for Dependency Injection
@@ -59,31 +84,30 @@ export function configureRedux() {
5984
getDefaultMiddleware().concat([middleware]),
6085
});
6186

62-
return store;
87+
return {
88+
store,
89+
// only for convenient type inference
90+
stateType: null as unknown as InferredState,
91+
};
6392
}
64-
```
65-
66-
```ts
67-
// State.ts
68-
import { configureRedux } from "./configureRedux";
6993

70-
// infer type from reducers registration
71-
export type State = ReturnType<ReturnType<typeof configureRedux>["getState"]>;
94+
type ConfiguredRedux = ReturnType<typeof configureRedux>;
95+
export type State = ConfiguredRedux['stateType'];
7296
```
7397

7498
Create a controller to encapsulate a piece of application logic.
7599
```ts
76100
// User.controller.ts
77-
import { ControllerBase, createAction, watch } from '@tomas_light/react-redux-controller';
78-
import { State } from "./State";
79-
import { UsersStore } from "./Users.store";
101+
import { ControllerBase, createAction, watch } from 'redux-controller-middleware';
102+
import { State } from './configureRedux';
103+
import { UserStore } from './User.store';
80104

81105
// prepare the class to use static methods for creating of actions
82106
@watch
83107
export class UserController extends ControllerBase<State> {
84-
// just to shorcat store update calls
85-
private updateStore(partialStore: Partial<UsersStore>) {
86-
this.dispatch(createAction(UsersStore.update, partialStore));
108+
// just to shortcat store update calls
109+
private updateStore(partialStore: Partial<UserStore>) {
110+
this.dispatch(createAction(UserStore.update, partialStore));
87111
}
88112

89113
// add action creator with name of the method: { type: 'loadUserList' }
@@ -116,17 +140,16 @@ export class UserController extends ControllerBase<State> {
116140
}
117141

118142
// there are restrictions from decorators in TS - it cannot to change type of the decorated class,
119-
// so we should manually do it =(
120-
// WatchedController takes all methods of the class and adds type definition for static action creators
121-
const typedController = (UserController as unknown) as WatchedController<UserController>;
122-
export { typedController as UserController };
143+
// so we should manually cast types
144+
// WatchedController takes all public methods of the class and adds type definition for static action creators
145+
const userController = UserController as unknown as WatchedController<UserController>;
146+
export { userController as UserController };
123147
```
124148

125149
And now you can dispatch the controller actions from a component.
126150
```tsx
127-
// UserList.ts
128151
import { useDispatch } from 'react-redux';
129-
import { UserController } from './UserController';
152+
import { UserController } from './User.controller';
130153

131154
const UserList = () => {
132155
const dispatch = useDispatch();
@@ -144,14 +167,14 @@ const UserList = () => {
144167
That's it!
145168

146169

147-
#### <a name="usage"></a> Custom action names
170+
### <a name="custom-action-names"></a> Custom action names
148171

149172
You can use custom action name, like `@watch('myCustomActionName')`.
150173
In this case you should to pass name mapper type as second argument of `WatchedController`
151174

152175
```ts
153-
import { ControllerBase, watch, WatchedController } from '@tomas_light/react-redux-controller';
154-
import { State } from "./State";
176+
import { ControllerBase, watch, WatchedController } from 'redux-controller-middleware';
177+
import { State } from './configureRedux';
155178

156179
@watch
157180
export class UserController extends ControllerBase<State> {
@@ -161,17 +184,16 @@ export class UserController extends ControllerBase<State> {
161184
@watch('loadChatById') loadChatByIdFromSpecialStorage(action: Action<{ chatId: string }>) {/* ... */}
162185
}
163186

164-
const typedController = (UserController as unknown) as WatchedController<UserController, {
187+
const userController = UserController as unknown as WatchedController<UserController, {
165188
loadChatByIdFromSpecialStorage: 'loadChatById', // map original method name to the new one
166189
}>;
167-
export { typedController as UserController };
190+
export { userController as UserController };
168191
```
169192

170-
### <a name="controller-di"></a> Register dependencies
193+
### <a name="dependency-injection"></a> Dependency injection
171194

172195
```ts
173196
// api.ts
174-
175197
const metadata = <T>(constructor: T): T => constructor;
176198

177199
export class UserApi {
@@ -184,7 +206,9 @@ export abstract class AccessKey {
184206
abstract key: string;
185207
}
186208

187-
@metadata // need any decorator (read cheap-di docs)
209+
// need any decorator for adding metadata to the constructor
210+
// https://github.com/tomas-light/cheap-di#typescript
211+
@metadata
188212
export class UserStorage {
189213
constructor(private readonly accessKey: AccessKey) {}
190214

@@ -194,11 +218,10 @@ export class UserStorage {
194218
}
195219
```
196220

197-
198221
```ts
199222
// App.tsx
200-
import { useEffect } from 'react';
201223
import { container } from 'cheap-di';
224+
import { useEffect } from 'react';
202225
import { AccessKey } from './api';
203226

204227
const App = () => {
@@ -212,9 +235,9 @@ const App = () => {
212235

213236
```ts
214237
// User.controller.ts
215-
import { ControllerBase, watch } from '@tomas_light/react-redux-controller';
216-
import { State } from "./State";
238+
import { ControllerBase, Middleware, watch } from 'redux-controller-middleware';
217239
import { UserApi, UserStorage } from './api';
240+
import { State } from './configureRedux';
218241

219242
@watch
220243
export class UserController extends ControllerBase<State> {
@@ -234,8 +257,55 @@ export class UserController extends ControllerBase<State> {
234257
}
235258
}
236259

237-
const typedController = (UserController as unknown) as WatchedController<UserController>;
238-
export { typedController as UserController };
260+
const userController = UserController as unknown as WatchedController<UserController>;
261+
export { userController as UserController };
262+
```
263+
264+
### <a name="create-action"></a> createAction
265+
266+
You can define action creators by yourself;
267+
```ts
268+
import { createAction } from 'redux-controller-middleware';
269+
270+
export class UsersActions {
271+
static LOAD_USER_LIST = 'LOAD_USER_LIST';
272+
static loadUserList = () => createAction(UsersActions.LOAD_USER_LIST);
273+
274+
static LOAD_USER = 'LOAD_USER';
275+
static loadUser = (data: { userID: string }) => createAction(UsersActions.LOAD_USER, data);
276+
}
277+
```
278+
279+
### <a name="chaining-action"></a> Chaining actions
280+
281+
You can chain action one by one:
282+
283+
```tsx
284+
import { useDispatch } from 'react-redux';
285+
import { chainActions } from 'redux-controller-middleware';
286+
import { UserController } from './User.controller';
287+
288+
const UserList = () => {
289+
const dispatch = useDispatch();
290+
291+
useEffect(() => {
292+
const action = chainActions(
293+
UserController.loadProfile({ userID: '123' }),
294+
UserController.openUserPage({ userID: '123' }),
295+
// ... any other
296+
);
297+
// same as
298+
const action = UserController.loadProfile({ userID: '123' });
299+
action.addNextActions(
300+
UserController.openUserPage({ userID: '123' }),
301+
// ... any other
302+
);
303+
304+
dispatch(action);
305+
}, []);
306+
307+
return <>...</>;
308+
};
239309
```
240310

241311
### <a name="without-decorators"></a> Without decorators
@@ -244,7 +314,7 @@ If you can't use decorators, you have to add watcher to your controller.
244314

245315
```ts
246316
// User.watcher.ts
247-
import { watcher } from '@tomas_light/react-redux-controller';
317+
import { watcher } from 'redux-controller-middleware';
248318
import { UserActions } from './User.actions';
249319
import { UserController } from './User.controller';
250320

@@ -258,7 +328,7 @@ export const UserWatcher = watcher(UserController,
258328

259329
```ts
260330
// controllerWatchers.ts
261-
import { Watcher } from '@tomas_light/react-redux-controller';
331+
import { Watcher } from 'redux-controller-middleware';
262332
import { UserWatcher } from '/User.watcher';
263333

264334
const controllerWatchers: Watcher[] = [
@@ -270,9 +340,9 @@ export { controllerWatchers };
270340
```
271341
```ts
272342
// configRedux.ts
273-
import { combineReducers } from "redux";
274-
import { controllerMiddleware } from "@tomas_light/react-redux-controller";
275-
import { controllerWatchers } from "./controllerWatchers";
343+
import { combineReducers } from 'redux';
344+
import { controllerMiddleware } from 'redux-controller-middleware';
345+
import { controllerWatchers } from './controllerWatchers';
276346

277347
export function configureRedux() {
278348
const rootReducer = combineReducers(/*...*/);
@@ -287,17 +357,3 @@ export function configureRedux() {
287357
return store;
288358
}
289359
```
290-
291-
### <a name="action-creating"></a> Action creating
292-
You can define action creators by yourself;
293-
```ts
294-
import { createAction } from "@tomas_light/react-redux-controller";
295-
296-
export class UsersActions {
297-
static LOAD_USER_LIST = 'LOAD_USER_LIST';
298-
static loadUserList = () => createAction(UsersActions.LOAD_USER_LIST);
299-
300-
static LOAD_USER = 'LOAD_USER';
301-
static loadUser = (data: { userID: string }) => createAction(UsersActions.LOAD_USER, data);
302-
}
303-
```

jest.config.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
{
22
"preset": "ts-jest",
3-
"testEnvironment": "jsdom"
3+
"testEnvironment": "jsdom",
4+
"coverageDirectory": "<rootDir>/test-coverage",
5+
"coverageReporters": ["json", "html"],
6+
"collectCoverageFrom": [
7+
"<rootDir>/src/**/*.{ts,tsx}"
8+
],
9+
"transform": {
10+
"^.+\\.tsx?$": [
11+
"ts-jest"
12+
]
13+
}
414
}

0 commit comments

Comments
 (0)