Skip to content

Instantly share code, notes, and snippets.

@nauzilus
Last active May 30, 2018 10:15
Show Gist options
  • Save nauzilus/289f5ee1eb410c0ba5193a877aeee66d to your computer and use it in GitHub Desktop.
Save nauzilus/289f5ee1eb410c0ba5193a877aeee66d to your computer and use it in GitHub Desktop.
poor mans redux
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