Skip to content

Commit

Permalink
wrote out more scaffolding code for usage and api
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas Chen committed May 18, 2017
1 parent bfb5b79 commit c9a7a2f
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 82 deletions.
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
"[typescript]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": true
},
"[markdown]": {
"editor.formatOnSave": true,
"editor.wordwrap": "on",
"editor.renderWhitespace": "all",
"editor.acceptSuggestionOnEnter": false
}
}
32 changes: 27 additions & 5 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,41 @@ By declaring metadata on your reducers, you can now subscribe to just changes in
## Usage
Decorate your reducers with metadata regarding what state it reads and what state it updates like so:

(without decorators)
```javascript
import Actions from './actions';
import ReduxArrows from 'redux-arrows';

const Reducers = {
@reads('posts.post', 'activeUser')
@writes('activePost')
myReducer(state, action) {
[Actions.SOME_TYPE]: ReduxArrows.redArrow('someProp', (state, action) => {
/* write your reducer here */
}),
[Actions.ANOTHER_TYPE]: ReduxArrows.redArrow('someProp', 'anotherProp', (state, action) => {
/* write your reducer here */
}).
[Actions.THIRD_TYPE]: (state, action) => {
// regular reducer
}
}

const arrowReducer = ReduxArrows.combineArrows(Reducers);

const { state, meta } = ReduxArrows.runKleisli(arrowReducer, { state: myState, action: someAction });
```
Here, state is the next state as traditionally manufactured by a reducer.

Then, subscribe to just the stuff that changes later on:
Meta is an object that contains the following:
```javascript
meta = {
changedKeys: ['someProp']
}
```
That is, in addition to giving you the state, we also give you what portions of the state changed with respect to the previous state. We do this so you can subscribe to only the stuff you're interested in on the store.

```javascript
this.store.subscribe('activePost', myCallback);
store.subscribe('activePost', myCallback);
```

This, in turn, unifies the MobX / Ember `computed` pattern of data management with redux's functional paradigm... and we get there by being even more declarative in redux

## API
39 changes: 30 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import ReducerArrow from './reducer-arrow';
import normalizeArrows from './normalize-arrows';
import { isBlank } from './utils';

export function redArrow(...args) {
const [...keys] = args.slice(0, -1)
const [redFn] = args.slice(-1)

class ReadingFunction extends ExtensibleFunction {
constructor(fn, depKeys=[]) {
super(fn);
this.depKeys = depKeys;
}
return ReducerArrow.create(redFn, keys)
}

export function reads(...args) {
const depKeys = args.slice(0, -1);
const fn = args[args.length - 1];
export function combineArrows(arrowsReducersHash) {
const arrowsHash = normalizeArrows(arrowsReducersHash)

return new ReadingFunction(depKeys, fn);
return ReducerArrow.create((state, action) => {
const keys = Object.keys(arrowsHash)
const actionState = { state, action }
const nextState = {}
let hasChanged = false
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const arrow = arrowsHash[key]
const {
state: nextStateForKey,
meta: { changedKeys }
} = ReducerArrow.runKleisli(arrow, actionState)

if (isBlank(nextStateForKey)) {
throw 'undefined state error'
}
nextState[key] = nextStateForKey
hasChanged = changedKeys.length > 0
}
return hasChanged ? nextState : state
});
}
59 changes: 59 additions & 0 deletions src/normalize-arrows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import ReduxArrow from './redux-arrow';

const enum CombinableType {
reducer,
arrow,
unknown
}

function typeOf(x: any): CombinableType {
if (ReduxArrow.isArrow(x)) {
return CombinableType.arrow;
}

if (typeof x === 'function') {
return CombinableType.reducer;
}

return CombinableType.unknown;
}

const NORMALIZERS_BY_TYPE: TypedNormalizers = {
[CombinableType.reducer](reducer) {
return ReduxArrow.arr(reducer)
},
[CombinableType.arrow](arrow) {
return arrow
},
[CombinableType.unknown]() {
return ReduxArrow.id
}
}

interface ArrowMaker {
(x: any): ReduxArrow
}

interface TypedNormalizers {
[propName: number]: ArrowMaker
}

interface ArrowsHash {
[propName: string]: ReduxArrow
}

interface UserInputHash {
[propName: string]: any
}

export default function normalizeArrows(arrowsReducersHash: UserInputHash): ArrowsHash {
const keys = Object.keys(arrowsReducersHash);
const arrows = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const thing = arrowsReducersHash[key];

arrows[key] = NORMALIZERS_BY_TYPE[typeOf(thing)](thing);
}
return arrows;
}
54 changes: 0 additions & 54 deletions src/reducer-arrow.ts

This file was deleted.

21 changes: 21 additions & 0 deletions src/redux-ext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Tuple } from './utils';
import { Action, ReduxState } from './redux';

export type Keys = string[]
export const enum ReducerStatusCode {
ok, unused
}

export interface ReducerMeta {
changedKeys: Keys
statusCode: ReducerStatusCode
}

export interface ActionState {
action: Action;
state: ReduxState;
}

export interface ReducerExt {
(actionState: ActionState): Tuple<ReduxState, ReducerMeta>
}
6 changes: 1 addition & 5 deletions src/redux.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
export type Action = any;
export type ReduxState = any;

export interface ActionState {
action: Action;
state: ReduxState;
}

export interface Reducer {
(state: ReduxState, action: Action): ReduxState
}

17 changes: 17 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,20 @@ function setCore(keys: string[], val: any, obj?: Hash): Hash {
export function parseKeys(keyStr: string): string[] {
return keyStr.split('.');
}

export class Tuple<A, B> {
static create(a, b) {
return new Tuple(a, b);
}
first: A
second: B
constructor(first, second) {
this.first = first;
this.second = second;
}
get last(): B {
return this.second;
}
}

export function isBlank(x: any): boolean { return typeof x === 'undefined'; }
37 changes: 30 additions & 7 deletions test/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { set } from '../src/utils';

describe('utils', () => {
it('should be propertly imported', () => {
expect(set).toBeInstanceOf(Function)
})

describe('set', () => {
const obj = { dog: 1 };
let obj2;
beforeAll(() => {
obj2 = set(obj, 'cat', 2)
})
it('should be propertly imported', () => {
expect(set).toBeInstanceOf(Function)
})
it('should immutably set to the object', () => {
expect(obj2).not.toBe(obj)
})
Expand All @@ -21,7 +20,16 @@ describe('utils', () => {
expect(obj2).toMatchObject({ dog: 1, cat: 2 })
})

describe('deep set', () => {
describe('deep', () => {
const expectedObj3 = {
dog: 1,
cat: 2,
bird: {
plane: {
man: 44
}
}
}
let obj3;
beforeAll(() => {
obj3 = set(obj2, 'bird.plane.man', 44)
Expand All @@ -33,14 +41,29 @@ describe('utils', () => {
expect(obj2).toMatchObject({ dog: 1, cat: 2 })
})
it('should properly create a deep object', () => {
expect(obj3).toMatchObject({
expect(obj3).toMatchObject(expectedObj3)
})

describe('existing', () => {
const expectedObj4 = {
dog: 1,
cat: 2,
bird: {
plane: {
man: 44
man: 44,
superman: 666
}
}
}
let obj4;
beforeAll(() => {
obj4 = set(obj3, 'bird.plane.superman', 666)
})
it('should not alter obj3', () => {
expect(obj3).toMatchObject(expectedObj3)
})
it('should put stuff into obj4', () => {
expect(obj4).toMatchObject(expectedObj4)
})
})
})
Expand Down
28 changes: 26 additions & 2 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@
"tslint:recommended"
],
"jsRules": {},
"rules": {},
"rules": {
"max-line-length": {
"options": [120]
},
"new-parens": true,
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": false,
"no-console": {
"options": [
"debug",
"info",
"log",
"time",
"timeEnd",
"trace"
]
},
"globals": {
"describe": true,
"it": true,
"expect": true
}
},
"rulesDirectory": []
}
}

0 comments on commit c9a7a2f

Please sign in to comment.