Last active
May 30, 2018 10:15
-
-
Save nauzilus/289f5ee1eb410c0ba5193a877aeee66d to your computer and use it in GitHub Desktop.
poor mans redux
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface IAction<T> { | |
type: string; | |
payload: T; | |
} | |
interface IUnsub { | |
(): void; | |
} | |
interface IStateListener<IState> { | |
(state: IState): void; | |
} | |
interface IStore { | |
registerEffect<TAction, TNext>(effect: IEffect<IAction<TAction>, IAction<TNext>>): void; | |
registerReducer<TState, TAction extends IAction<TAction>>(reducer: IReducer<TState, TAction>): void; | |
listen<T>(onUpdate: IStateListener<T>): IUnsub; | |
dispatch<T>(action: IAction<T>): void; | |
currentState<T>(): T; | |
} | |
interface IReducer<TState, TAction extends IAction<any>> { | |
(state: TState, action: TAction): TState; | |
} | |
interface IEffect<TAction, TNext> { | |
(action: IAction<TAction>): Promise<IAction<TNext>>; | |
} | |
class Store implements IStore { | |
private effects: IEffect<any, any>[] = []; | |
private reducers: IReducer<any, any>[] = []; | |
private listeners: IStateListener<any>[] = []; | |
private state: any[] = []; | |
constructor() { | |
} | |
public currentState<T>(): T { | |
return this.state.length | |
? <T>this.state.slice(-1)[0].state | |
: void 0; | |
} | |
public registerEffect<TAction, TNextAction>(effect: IEffect<TAction, TNextAction>): void { | |
if (this.effects.indexOf(effect) < 0) { | |
this.effects.push(effect); | |
} | |
} | |
public registerReducer<TState, TAction extends IAction<any>>(reducer: IReducer<TState, TAction>): void { | |
if (this.reducers.indexOf(reducer) < 0) { | |
this.reducers.push(reducer); | |
} | |
} | |
public listen<T>(onUpdate: IStateListener<T>): () => void { | |
if (this.listeners.indexOf(onUpdate) < 0) { | |
this.listeners.push(onUpdate); | |
this.notify(onUpdate); | |
} | |
return () => { | |
const index = this.listeners.indexOf(onUpdate); | |
if (index >= 0) { | |
this.listeners.splice(index, 1); | |
} | |
}; | |
} | |
public dispatch<T>(action: IAction<T>): void { | |
this.effects | |
.map(effect => effect(action)) | |
.filter(Boolean) | |
.forEach(effect => effect.then(next => this.dispatch(next))); | |
const currentState = this.currentState(); | |
const nextState = this.reducers.reduce((state, reducer) => reducer(state, action) || state, currentState); | |
if (currentState !== nextState) { | |
this.state.push({ action, state: nextState }); | |
this.notify(); | |
} | |
} | |
private notify(onUpdate?: IStateListener<any>): void { | |
const state = this.currentState(); | |
if (state) { | |
if (onUpdate) { | |
onUpdate(state); | |
} else { | |
this.listeners.forEach(listener => listener(state)); | |
} | |
} | |
} | |
} | |
class IncreaseAction implements IAction<number> { | |
public readonly type: string = 'INCREASE'; | |
constructor(public payload: number) { | |
this.payload = payload; | |
} | |
} | |
class DecreaseAction implements IAction<number> { | |
public readonly type: string = 'DECREASE'; | |
constructor(public payload: number) { | |
this.payload = payload; | |
} | |
} | |
class SetAction implements IAction<number> { | |
public readonly type: string = 'SET'; | |
constructor(public payload: number) { | |
this.payload = payload; | |
} | |
} | |
class GetDateAction implements IAction<void> { | |
public readonly type: string = 'NOW'; | |
public payload: void; | |
constructor() { | |
} | |
} | |
class GetDateSuccessAction implements IAction<number> { | |
public readonly type: string = 'NOW_DONE'; | |
constructor(public payload: number) { | |
} | |
} | |
class RewindAction implements IAction<boolean> { | |
public readonly type: string = 'NOTHINGMAN'; | |
constructor(public payload: boolean) { | |
this.payload = payload; | |
} | |
} | |
const currentTime: IEffect<void, number> = (action: GetDateAction) => { | |
switch (action.type) { | |
case 'NOW': | |
return Promise.resolve(Date.now()).then(now => new GetDateSuccessAction(now)); | |
} | |
}; | |
interface IValueState { | |
value: number; | |
loading: boolean; | |
} | |
type ValueAction = IncreaseAction | DecreaseAction | SetAction; | |
const valueReducer: IReducer<IValueState, ValueAction> = (state = { value: 0, loading: false }, action: ValueAction) => { | |
switch (action.type) { | |
case 'INCREASE': | |
return { ...state, value: state.value + action.payload }; | |
case 'DECREASE': | |
return { ...state, value: state.value - action.payload }; | |
case 'SET': | |
return { ...state, value: action.payload }; | |
case 'NOW': | |
return { ...state, loading: true }; | |
case 'NOW_DONE': | |
return { ...state, loading: false, value: action.payload }; | |
} | |
}; | |
const store = new Store(); | |
store.registerReducer(valueReducer); | |
store.registerEffect(currentTime); | |
const unsub = store.listen<IValueState>( | |
state => console.log(state) | |
); | |
store.dispatch(new IncreaseAction(5)); | |
store.dispatch(new RewindAction(true)); // does nothing | |
store.dispatch(new DecreaseAction(3)); | |
store.dispatch(new GetDateAction()); // async! dispatches run out of order | |
store.dispatch(new SetAction(13)); | |
unsub(); | |
store.dispatch(new SetAction(42)); | |
store.listen<IValueState>( | |
state => console.log('resubbed', state) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment