Skip to content

Instantly share code, notes, and snippets.

@codyromano
Created July 31, 2016 19:47
Show Gist options
  • Select an option

  • Save codyromano/b2e918a534819ccd720b2cfe1a13cb5c to your computer and use it in GitHub Desktop.

Select an option

Save codyromano/b2e918a534819ccd720b2cfe1a13cb5c to your computer and use it in GitHub Desktop.
interface StoreOptions {
/* The configuration object passed to the constructor of Store.
This is where you specify the application's initial state
along with reducers, pure functions that handle actions. */
state: StoreState;
reducers: Array<StoreReducer>;
}
interface StoreAction {
/* Actions are object literals that basically express how the application
state should change. They're the only way you can change it. */
type: string;
[others: string]: any;
}
interface StoreReducer {
/* A reducer function applies an action to the current state and
returns a copy of the updated state:
reducer(state, action) => state
Reducers must never mutate the original state. */
(state: StoreState, action: StoreAction): StoreState;
}
interface StoreAPI {
// Get the entire current state
getState: Function;
// Publish an action that triggers a new state
publish: Function;
}
interface StoreState {
// Allow properties to be added at runtime
[prop: string]: any
}
function clone(obj) {
return (<any>Object).assign({}, obj);
}
class Store implements StoreAPI {
/* All published actions. Only the publish() method is allowed to push
to the array. Otherwise the state cache won't work properly. */
private actions: Array<StoreAction> = [];
private reducers: Array<StoreReducer> = [];
private state: StoreState;
/* Cache state so we don't have to replay all the
actions every time getState() is called */
private cachedState: StoreState = null;
private currentActionsIndex: number = 0;
private actionsIndexAtLastGet: number = 0;
constructor(options: StoreOptions) {
let {state, reducers} = options;
// Initial state of the app
this.state = Object.freeze(state);
this.reducers = reducers;
}
publish(action: StoreAction): boolean {
if (this.validateAction(action)) {
this.currentActionsIndex = this.actions.push(action);
return true;
}
return false;
}
getState(): StoreState {
let totalActions = this.actions.length;
// If no actions have been published, return a copy of state as-is
if (totalActions === 0) {
return clone(this.state);
}
if (this.cachedStateIsValid()) {
return this.cachedState;
}
// If cache is invalid, process actions added after the data was cached
let newActions = this.actions.slice(this.actionsIndexAtLastGet);
// Replay all the actions to compute our new state
this.cachedState = newActions.reduce((stateCopy, action) => {
// Pass the action to each of the reducer functions
this.reducers.forEach((reducer) => {
stateCopy = <StoreState>reducer(stateCopy, action);
});
return stateCopy;
}, this.state);
this.actionsIndexAtLastGet = totalActions;
return this.cachedState;
}
private cachedStateIsValid(): boolean {
/* If a cached copy of state exists and no actions have been
published since it was cached, return the cached copy. */
return (this.currentActionsIndex === this.actionsIndexAtLastGet);
}
private validateAction(action: StoreAction): boolean {
if (typeof action.type !== 'string') {
console.error('Action is missing a type: ', action);
return false;
}
return true;
}
}
function planetReducer(state, action): StoreState {
let newState = clone(state);
switch (action.type) {
case 'ADD_PLANET':
newState.planets.push({
name: action.name,
visited: action.visited
});
break;
case 'EDIT_PLANET':
for (let i=0, l=newState.planets.length; i<l; i++) {
let planet = newState.planets[i];
if (planet.name === action.name) {
planet.name = action.newName;
break;
}
}
break;
default:
console.warn(`Unexpected action type: "${action.type}"`);
break;
}
return newState;
}
let store = new Store({
// The initial state of the whole application
state: {
planets: [
{
name: 'Earth',
visited: true
},
{
name: 'Pluto',
visited: false
}
]
},
/* I'm only using one reducer, but you can use multiple. Redux calls this
reducer composition and it's a good technique for organizing your
reducers in large applications.*/
reducers: [planetReducer]
});
store.publish({
type: 'ADD_PLANET',
name: 'Saturn',
visited: false
});
store.publish({
type: 'EDIT_PLANET',
name: 'Pluto',
newName: 'Mars'
});
let newState = store.getState();
console.assert(newState.planets.length === 3, 'Planet was added');
console.assert(newState.planets[1].name === 'Mars', 'Planet name edited');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment