Last active
May 3, 2018 01:28
-
-
Save soundyogi/fb6ce0aa35435bc095608b227e8c5d93 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
| /* FLUX and UNIDIRECTIONAL DATAFLOW | |
| GOAL: We will build a very simple Redux-Like Flux in this File. | |
| Flux is the name for a pattern developed by facebook. | |
| Intead of relying on React to manage state - we just let it do what it is good at. | |
| Being a View Library. | |
| With an external Datastore and Discrete "Actions" that transform it, | |
| "Transactional State" can be achieved. | |
| It also enables clean and seperate pipelines between - external input "events" like | |
| the user clicking a button - and the data that actually has to change. | |
| This is called: "Unidirectional Dataflow" | |
| Dispatcher -> State Store -> View -> Dispatcher -> State Store -> View -> Dispatcher etc... | |
| Dispatcher -> Store -> View -> Dispatcher | |
| (Send Action) (State Transformation) (Render) (Send Action/Event/Command) | |
| */ | |
| function createStore(rootReducer, initialState, storeEnhancer) { | |
| const createStore = storeEnhancer | |
| ? storeEnhancer(_createStore) | |
| : _createStore | |
| ; | |
| return createStore(); | |
| function _createStore() { | |
| if (!rootReducer) throw Error('rootReducer is missing') | |
| const _mapSubscribers = {} | |
| const _rootReducer = rootReducer | |
| const _store = { | |
| dispatch: baseDispatch, | |
| getState, | |
| subscribe | |
| } | |
| let _state = initialState | |
| return _store; | |
| function getState() { | |
| return _state | |
| } | |
| function subscribe(f) { | |
| _mapSubscribers[uuidv4()] = f | |
| } | |
| function baseDispatch(action) { | |
| if (!action || !action.type) throw Error("cant call dispatch without action. stupid."); | |
| _state = _rootReducer(_state, action) | |
| for (var subscriberKey in _mapSubscribers) | |
| _mapSubscribers[subscriberKey] ? _mapSubscribers[subscriberKey]() : null; | |
| return true; | |
| } | |
| } | |
| } | |
| function applyMiddleware(...middlewares) { | |
| return createStore => (...args) => { | |
| const store = createStore(...args) | |
| let chain = [] | |
| let dispatch | |
| const middlewareAPI = { | |
| getState: store.getState, | |
| dispatch: (...args) => dispatch(...args) | |
| } | |
| chain = middlewares.map(middleware => middleware(middlewareAPI)) | |
| dispatch = compose(...chain)(store.dispatch) | |
| return { | |
| ...store, | |
| dispatch | |
| } | |
| } | |
| } | |
| // Middleware | |
| // | |
| const logger = store => next => action => { | |
| if(!action.type && action instanceof Function) console.log('thunk: '+action.name) | |
| else { console.info('event: ', action) } | |
| let result = next(action) | |
| return result | |
| } | |
| const crashReporter = store => next => action => { | |
| try { | |
| return next(action) | |
| } catch (err) { | |
| console.error('Caught an exception!', err) | |
| throw err | |
| } | |
| } | |
| const vanillaPromise = store => next => action => { | |
| if (typeof action.then !== 'function') { | |
| return next(action) | |
| } | |
| return Promise.resolve(action).then(store.dispatch) | |
| } | |
| const thunk = store => next => action => | |
| typeof action === 'function' | |
| ? action(store.dispatch, store.getState) | |
| : next(action) | |
| /* | |
| function combineReducers(reducerObject:Object):function{} | |
| Combines Reducers by returning a Reducer Reducing Reducers | |
| */ | |
| function combineReducers(reducerObject){ | |
| function rootReducer(state, action){ | |
| const newState = {} | |
| Object.keys(reducerObject).reduce( (newState, currentKey) => { | |
| if(!state) state = {} | |
| // if(!state[currentKey]) state[currentKey] = undefined; | |
| newState[currentKey] = reducerObject[currentKey](state[currentKey], action) | |
| return newState | |
| }, newState) | |
| return newState | |
| } | |
| return rootReducer | |
| } | |
| /* UTIL | |
| */ | |
| // RFC4122 version 4 compliant | |
| function uuidv4() { | |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { | |
| var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); | |
| return v.toString(16); | |
| }); | |
| } | |
| // compose functions 'right to left' | |
| function compose(...funcs) { | |
| if (funcs.length === 0) { | |
| return arg => arg | |
| } | |
| if (funcs.length === 1) { | |
| return funcs[0] | |
| } | |
| return funcs.reduce((a, b) => (...args) => a(b(...args))) | |
| } | |
| // TEST HARNESS | |
| // | |
| function log(...x){ return console.log(...x) } | |
| function test(suiteName, f){ | |
| log(suiteName) | |
| f({ok}) | |
| } | |
| function ok(expr, msg){ | |
| expr ? log('%c!pass:'+msg, 'color:green') : log('%c?fail:'+msg, 'color:red') | |
| } | |
| test('selftest', function(t){ | |
| t.ok(true, ' I pass') | |
| t.ok(false, 'I fail') | |
| }) | |
| /* TESTS | |
| */ | |
| test('createStore', t => { | |
| // Actions, Reducer, etc. | |
| const TOGGLE_ACTION = {type: "TOGGLE"} | |
| const ASYNC_ACTION = {type: "ASYNC"} | |
| function myActionCreator(){ return TOGGLE_ACTION } | |
| function myThunkCreator(){ return function thunk(dispatch, getState){ | |
| console.log(getState()) | |
| dispatch(ASYNC_ACTION) | |
| }} | |
| // you dont have to use switch statements in reducers | |
| const firstShape = { | |
| TOGGLE: false | |
| } | |
| const firstReducer = function(state = firstShape, action){ | |
| if(action.type === "TOGGLE"){ | |
| var newState = Object.assign(state) | |
| newState.TOGGLE = !newState.TOGGLE | |
| return newState | |
| } | |
| return state | |
| } | |
| // but youl will see it often, it has become standard | |
| const secondShape = { | |
| primitive:2, | |
| aReference: {}, | |
| async: null, | |
| } | |
| const secondReducer = function(state = secondShape, action){ | |
| switch(action.type) { | |
| case "ASYNC": { | |
| state.async = 'changedByAsyncThunk' | |
| } | |
| } | |
| return state | |
| } | |
| const rootReducer = combineReducers({ | |
| first: firstReducer, | |
| second: secondReducer, | |
| }) | |
| var STORE = createStore( rootReducer, null, applyMiddleware(logger, thunk) ) | |
| STORE.subscribe(function(){ log("subscriber called!") }) | |
| // TESTING | |
| t.ok(createStore, 'is defined') | |
| try { | |
| createStore() | |
| } catch (e){ | |
| t.ok(e instanceof Error, 'cannot create a store without a rootReducer') | |
| } | |
| t.ok(STORE.getState && STORE.dispatch && STORE.subscribe, 'all exports are defined') | |
| // initialize the reducers (redux does this behind the scenes) | |
| STORE.dispatch({type: Date.now()}) | |
| var currentState = STORE.getState() | |
| t.ok(currentState.first && currentState.first.TOGGLE === false, "inital state works as expected") | |
| try { STORE.dispatch() } | |
| catch (e){ | |
| t.ok(e instanceof Error, "dispatcher cannot dispatch undefined") | |
| } | |
| try { STORE.dispatch({notype: "no type"}) } | |
| catch (e){ | |
| t.ok(e instanceof Error, "actions MUST have a type") | |
| } | |
| t.ok(STORE.dispatch(TOGGLE_ACTION), "standard actions can be dispatched") | |
| currentState = STORE.getState() | |
| console.log(currentState) | |
| t.ok(currentState.first.TOGGLE === true, 'action trasformed state as expected') | |
| STORE.dispatch(myActionCreator()) | |
| t.ok(currentState.first.TOGGLE === false, "state transformation working as expected") | |
| STORE.dispatch(myThunkCreator()) | |
| t.ok(currentState.second.async === "changedByAsyncThunk", 'middleware works as expected') | |
| }) | |
| /* Co-Locating Action Creators, Thunk Creators, Async, Selectors etc. | |
| There are a few things that just make sense to package with a reducer | |
| since it makes the logic easy to find and standards are good for everybody | |
| Categories: | |
| Action Types | |
| Actions / Action Creators | |
| Async Dispatchers (Thunks, Sagas, Epics...) | |
| State Selectors | |
| Reducer | |
| etc. | |
| */ | |
| /* ACTION TYPES | |
| first problem: how to name actions | |
| are they events that HAPPENED or | |
| are they commands that are imperative? | |
| in any case, some kind of namespacing is always a good idea | |
| i.e | |
| const *NAMESPACE*_I_HAVE_HAPPENED | |
| const CAPTCHA_FAILED | |
| vs. | |
| const ACCOUNT_DELETE_ENTRY | |
| const CAPTCHA_RESOLVE | |
| */ | |
| const ACCOUNT_LIST_ENTRY_SELECT_CHANGED = "ACCOUNT_LIST_ENTRY_SELECT_CHANGED"; // use any UNIQUE value here, strings are nice for manually sending actions later | |
| const ROLE_ADD_BUILDER_SUBMITTED = "ROLE_ADD_BUILDER_SUBMITTED"; | |
| const ROLE_INPUT_BLURRED = "ROLE_INPUT_BLURRED"; | |
| const ROLE_INPUT_ADD = "ROLE_INPUT_ADD"; | |
| const ROLE_REMOVED = "ROLE_REMOVED"; | |
| /* ACTION CREATORS | |
| This will be the main way Components Describe what they want to happen | |
| e.g. onClick = someActionCreator(params) | |
| Actions look like this: | |
| interface Action { | |
| type: any; // as long as any is a UNIQUE value | |
| payload?: object; // payload is convention, I like it | |
| } | |
| There are places for lambdas, this is not one of them. | |
| Why delcare a name and then assign an ANONMOUS object to it? | |
| It messes up our stack traces. | |
| ANTIPATTERN: | |
| const someName = param => ({type: "BLA"}) | |
| */ | |
| function accountListEntrySelectChange(account) { | |
| return { type: ACCOUNT_LIST_ENTRY_SELECT_CHANGED, payload: account } | |
| } | |
| function roleAddBuilderSubmit() { | |
| return { type: ROLE_ADD_BUILDER_SUBMITTED } | |
| } | |
| function roleRemove(param) { | |
| return { type: ROLE_REMOVED, payload: { data: true, param } } | |
| } | |
| /* STATE TRANSFORMATION / REDUCERS | |
| A Reducer is just a Function that accepts the currentState and an action | |
| thay always has to return! | |
| No Side Effects may happen in Reducers! (e.g async calls, dispatch etc) | |
| This is one, if not the, point where REDUX differs from FLUX. | |
| And also this is where REDUX has gotten its name. | |
| Reducer Functions in REDUX are pure, meaning they only use their local scope. | |
| They receive the current state and current action as inputs and are expected to | |
| ALWAYS return state, be it new or old. | |
| Also, when updating arrays and objects you ALWAYS have to TRANSFORM the object | |
| NEVER. MUTATE. ANY. OBJECT. | |
| This is also called "Immutability". | |
| There are libs to enforce this, like immutable.js | |
| */ | |
| function someReducerFunction(state, action){ | |
| if(action.type === 'ADD_VALUE'){ | |
| // WRONG = mutates the array. | |
| // state.myList.push(action.payload) | |
| // CORRECT = a new reference will be created | |
| state.myList = [...state.myList, action.payload] // ... in this context is es6 array spread syntax | |
| // the ... is es6 array-spread syntax it does the same as: | |
| // state.myList = [].concat(state.myList, [action.payload]) | |
| } | |
| return state | |
| } | |
| // InitialState | |
| // It is nice to have the initial state colocated | |
| const AccountsShape = { | |
| rolesToAdd: {}, | |
| listedAccounts: [], | |
| selectAccountIds: [] | |
| } | |
| function AccountsReducer(state = AccountsShape, action) { | |
| const PL = action.payload | |
| switch (action.type) { | |
| case ACCOUNT_LIST_ENTRY_SELECT_CHANGED: { | |
| if (state.selectedAccountIds.includes(PL.account.id)) { | |
| state.selectedAccountIds = state.selectedAccountIds.slice(state.selectedAccountIds.indexOf(PL.account.id), 1); | |
| } else { | |
| state.selectedAccountIds = [...state.selectedAccountIds, PL.account.id] | |
| } | |
| } | |
| } | |
| return state | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment