Skip to content

Commit 8b88932

Browse files
author
keenondrums
committed
Make it work with inheritance
1 parent 6585e5b commit 8b88932

File tree

6 files changed

+175
-7
lines changed

6 files changed

+175
-7
lines changed

README.md

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Consider using it with [flux-action-class](https://github.com/keenondrums/flux-a
2222
- [Step 1](#step-1)
2323
- [Step 2](#step-2)
2424
- [How can I make shared reducer's logic dynamic?](#how-can-i-make-shared-reducers-logic-dynamic)
25+
- [Reducer inheritance](#reducer-inheritance)
2526
- [In depth](#in-depth)
2627
- [When can we omit list of actions for `@Action`?](#when-can-we-omit-list-of-actions-for-action)
2728
- [Running several reducers for the same action](#running-several-reducers-for-the-same-action)
@@ -669,9 +670,6 @@ const reducer = ReducerCat.create()
669670
```js
670671
import { Action, Extend, ReducerClass } from 'reducer-class'
671672

672-
interface IHungryState {
673-
hungry: boolean;
674-
}
675673
export const makeReducerHungry = (actionHungry, actionFull) =>
676674
class {
677675
@Action(actionHungry)
@@ -717,6 +715,76 @@ const reducer = ReducerCat.create()
717715

718716
</details>
719717

718+
## Reducer inheritance
719+
720+
Any reducer class is still a class, therefore it can be inherited. It's different way to share some common logic and alter the final behavior for children. There's no runtime information about method visibility (`private`, `protected`, `public`), so if you want to share some common logic without wrapping it with `@Action` decorator prefix the shared method with `_`.
721+
722+
```ts
723+
interface ICatState {
724+
enegry: number
725+
}
726+
class CatReducer extends ReducerClass<ICatState> {
727+
initialState = {
728+
energy: 10,
729+
}
730+
731+
@Action
732+
addEnergy(state: ICatState, action: ActionCatEat) {
733+
return this._addEnergy(state, action)
734+
}
735+
736+
// DO NOT FORGET TO PREFIX IT WITH "_"
737+
protected _addEnergy(state: ICatState, action: ActionCatEat): ICatState {
738+
return {
739+
energy: state.energy + action.payload,
740+
}
741+
}
742+
}
743+
744+
class KittenReducer extends CatReducer {
745+
// DO NOT FORGET TO PREFIX IT WITH "_"
746+
protected _addEnergy(state: ICatState, action: ActionCatEat): ICatState {
747+
return {
748+
energy: state.energy + action.payload * 10,
749+
}
750+
}
751+
}
752+
```
753+
754+
<details>
755+
<summary>JavaScript version</summary>
756+
757+
```js
758+
class CatReducer extends ReducerClass {
759+
initialState = {
760+
energy: 10,
761+
}
762+
763+
@Action(ActionCatEat)
764+
addEnergy(state, action) {
765+
return this._addEnergy(state, action)
766+
}
767+
768+
// DO NOT FORGET TO PREFIX IT WITH "_"
769+
protected _addEnergy(state, action) {
770+
return {
771+
energy: state.energy + action.payload,
772+
}
773+
}
774+
}
775+
776+
class KittenReducer extends CatReducer {
777+
// DO NOT FORGET TO PREFIX IT WITH "_"
778+
protected _addEnergy(state, action) {
779+
return {
780+
energy: state.energy + action.payload * 10,
781+
}
782+
}
783+
}
784+
```
785+
786+
</details>
787+
720788
## In depth
721789

722790
### When can we omit list of actions for `@Action`?

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "reducer-class",
3-
"version": "1.3.2",
3+
"version": "1.4.0",
44
"description": "Boilerplate free class-based reducer creator. Built with TypeScript. Works with Redux and NGRX. Has integration with immer.",
55
"main": "dist/index.js",
66
"scripts": {

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const METADATA_KEY_METHOD_PARAMS = 'design:paramtypes'
22
export const METADATA_KEY_ACTION = Symbol()
3+
export const PREFIX_OMIT_METHOD = '_'

src/reducer-class-helpers.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,45 @@ describe(ReducerClassHelpers.name, () => {
5252
expect(methodWithActionTypes2.actionType).toBe(Action2.type)
5353
expect(methodWithActionTypes2.method).toBe(mockAddImmerIfNeededResImmer)
5454
})
55+
test('returns action types for the whole inheritance chain', () => {
56+
class Action1 extends ActionStandard {}
57+
class Action2 extends ActionStandard {}
58+
class Test extends ReducerClass<undefined> {
59+
public initialState = undefined
60+
61+
@Action(Action1)
62+
public reducerPure(): undefined {
63+
return undefined
64+
}
65+
}
66+
class TestChild extends Test {
67+
@Action(Action2)
68+
public reducerImmer(state: undefined, draft: undefined, action: any) {
69+
return undefined
70+
}
71+
}
72+
const test = new TestChild()
73+
const keys = reducerClassHelpers.getClassInstanceMethodNames(test)
74+
const spyGetMetadata = jest.spyOn(Reflect, 'getMetadata')
75+
const mockAddImmerIfNeededResPure = Symbol()
76+
const mockAddImmerIfNeededResImmer = Symbol()
77+
jest.spyOn(ReducerClassHelpers.prototype, 'addImmerIfNeeded').mockImplementation(
78+
(reducer: any): any => {
79+
if (reducerClassHelpers.typeGuardReducerPure(reducer)) {
80+
return mockAddImmerIfNeededResPure
81+
}
82+
return mockAddImmerIfNeededResImmer
83+
},
84+
)
85+
const methodsWithActionTypes = reducerClassHelpers.getReducerClassMethodsWthActionTypes(test as any, keys)
86+
expect(spyGetMetadata).toBeCalledTimes(2)
87+
expect(methodsWithActionTypes.length).toBe(2)
88+
const [methodWithActionTypes1, methodWithActionTypes2] = methodsWithActionTypes
89+
expect(methodWithActionTypes1.actionType).toBe(Action1.type)
90+
expect(methodWithActionTypes1.method).toBe(mockAddImmerIfNeededResPure)
91+
expect(methodWithActionTypes2.actionType).toBe(Action2.type)
92+
expect(methodWithActionTypes2.method).toBe(mockAddImmerIfNeededResImmer)
93+
})
5594
test('throws if no actions passed', () => {
5695
class Test extends ReducerClass<undefined> {
5796
public initialState = undefined
@@ -65,6 +104,25 @@ describe(ReducerClassHelpers.name, () => {
65104
MetadataActionMissingError,
66105
)
67106
})
107+
test("doesn't throw on methods prefixed with '_' if no actions passed", () => {
108+
class Test extends ReducerClass<undefined> {
109+
public initialState = undefined
110+
public _test3() {
111+
return undefined
112+
}
113+
protected _test2() {
114+
return undefined
115+
}
116+
117+
// @ts-ignore
118+
private _test() {
119+
return undefined
120+
}
121+
}
122+
const test = new Test()
123+
const keys = reducerClassHelpers.getClassInstanceMethodNames(test)
124+
expect(keys).toEqual([])
125+
})
68126
})
69127

70128
describe(ReducerClassHelpers.prototype.getReducerMap.name, () => {

src/reducer-class-helpers.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { produce } from 'immer'
22
import { DeepReadonlyArray, DeepReadonlyObject } from 'typelevel-ts'
33

4-
import { METADATA_KEY_ACTION } from './constants'
4+
import { METADATA_KEY_ACTION, PREFIX_OMIT_METHOD } from './constants'
55
import { MetadataActionMissingError } from './errors'
6+
import { ReducerClass } from './reducer-class'
67

78
export type Immutable<T> = T extends object ? DeepReadonlyObject<T> : T extends any[] ? DeepReadonlyArray<T> : T
89
export interface IReducerMap<T> {
@@ -65,8 +66,14 @@ export class ReducerClassHelpers {
6566
return accum
6667
}, {})
6768
}
68-
public getClassInstanceMethodNames(obj: object) {
69+
public getClassInstanceMethodNames(obj: object): string[] {
6970
const proto = Object.getPrototypeOf(obj)
70-
return Object.getOwnPropertyNames(proto).filter((name) => name !== 'constructor')
71+
if (!(proto instanceof ReducerClass)) {
72+
return []
73+
}
74+
const protoMethodNames = Object.getOwnPropertyNames(proto).filter(
75+
(name) => name !== 'constructor' && !name.startsWith(PREFIX_OMIT_METHOD),
76+
)
77+
return [...this.getClassInstanceMethodNames(proto), ...protoMethodNames]
7178
}
7279
}

src/reducer-class.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,38 @@ describe(ReducerClass.name, () => {
4747
sum: 20,
4848
})
4949
})
50+
test('can be inherited', () => {
51+
interface ITestState {
52+
sum: number
53+
}
54+
class Action1 extends ActionStandard {}
55+
class Test extends ReducerClass<ITestState> {
56+
public initialState = { sum: 10 }
57+
58+
@Action
59+
public test1(state: ITestState, action: Action1) {
60+
return this._sum(state)
61+
}
62+
63+
protected _sum(state: ITestState): ITestState {
64+
return {
65+
sum: state.sum + 1,
66+
}
67+
}
68+
}
69+
70+
class TestChild extends Test {
71+
protected _sum(state: ITestState): ITestState {
72+
return {
73+
sum: state.sum + 100,
74+
}
75+
}
76+
}
77+
78+
const testReducer = TestChild.create()
79+
const res11 = testReducer(undefined, new Action1())
80+
expect(res11).toEqual({
81+
sum: 110,
82+
})
83+
})
5084
})

0 commit comments

Comments
 (0)