Created
July 31, 2016 19:47
-
-
Save codyromano/b2e918a534819ccd720b2cfe1a13cb5c to your computer and use it in GitHub Desktop.
This file contains hidden or 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 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