5
\$\begingroup\$

I'm trying to better understand flux-style architecture, so I wrote this snippet to clarify how a simple flux system can be designed. I was wondering if my implementation is valid, or if I've missed something.

const application = () => {
  /* Init the model */
  let model = {
    page: null
  };
  
  /* redirects the reference from the old state to the new */
  const updateState = newState => model = newState;
  
  /* a reducer method, more are easily added */
  const UPDATE_PAGE = (model, newPage) => {
    let newState = Object.assign({}, model);
    newState.page = newPage;
    updateState(newState);
  };
  
  /* 
   * dispatch is a core function, directing every action
   * to the proper reducer depending on the `type`
   */
  const dispatch = action => {
    action.type === 'UPDATE_PAGE' ? 
      UPDATE_PAGE(model, action.page)
      : null;
  };
  
  /* An action mutating state */
  let updatePageAction = {
    type: 'UPDATE_PAGE',
    page: 1
  };
  
  console.log('initial model: ', model);
  
  /* simply dispatch the action */
  dispatch(updatePageAction);
  
  /* and the state is updated! */
  console.log('updated model: ', model);
};

application();

\$\endgroup\$
  • \$\begingroup\$ Hey there. Please do not edit your question to include an updated version of your code. That is highly confusing and makes it difficult for late reviewers to review your code. If you want to solicit further feedback, then you should post a new question instead. Thanks \$\endgroup\$ – Vogel612 Apr 13 '18 at 7:28
  • 1
    \$\begingroup\$ Please see what you may and may not do after receiving answers. \$\endgroup\$ – Heslacher Apr 13 '18 at 7:30
3
\$\begingroup\$

First of all, Flux is not to be confused with Redux. While both have similarities, the main difference is that Flux allows multiple stores while Redux advises only using one store per app for simplicity. The following will talk about Redux.

/* 
 * dispatch is a core function, directing every action
 * to the proper reducer depending on the `type`
 */
const dispatch = action => {
  action.type === 'UPDATE_PAGE' ? 
    UPDATE_PAGE(model, action.page)
    : null;
};

Dispatch doesn't actually "direct" the action to the right reducer. It simply invokes the reducer, supplying it the current state and the dispatched action. It's actually the reducer that does the pattern matching/selection of what logic runs with which action.

/* a reducer method, more are easily added */
const UPDATE_PAGE = (model, newPage) => {
  let newState = Object.assign({}, model);
  newState.page = newPage;
  updateState(newState);
};

A reducer is a pure function which takes 2 arguments, the current state and the dispatched action and returns the new state, based on the state and the passed action. You can think of it as the "equation" part of the app (yes, like a math equation). Reducers look like:

const initialState = { count: 0 }

const reducer = (state = initialState, action) => {
  switch(action.type){
    case 'INCREMENT':
      return {...state, count: state.count + 1 }
    case 'DECREMENT'
      return {...state, count: state.count - 1 }
    default:
      return state
  }
}

There's several reasons why reducers are designed this way.

  • The reducer is pure. Its result is not affected by anything other than its inputs, making it stupid easy to test.
  • It's just a data transformer. You can easily test the logic without fancy mocks, stubs or spies because there's nothing to mock, stub or spy.
  • You can easily compose reducers. You can divide the state into sections, each with its own reducer. Redux provides a handy combineReducers utility function for it.
  • At the end of the day, reducers are just functions. Not a fancy service class, not a magical method, not some fancy concept invented by someone, it's just a function.

Redux is so simple, that if you just overheard someone in the pub talking about what it does, you can easily implement it yourself. It's effectively a pub-sub library, except that it also manages and holds state, and only ever emits one kind of event - a state change.

// my-redux.js - The only part that is "Redux"
export const createStore = reducer => {
  const subscribers = []
  let currentState = null

  return {
    getState(){
      return currentState
    },
    subscribe(fn){
      subscribers.push(fn)
      fn(currentState)
    },
    dispatch(action){
      currentState = reducer(currentState, action)
      subscribers.forEach(fn => fn.currentState)
    }
  }
}

// reducer.js - Where your app's logic goes. 
const initialState = { count: 0 }

export const reducer = (state = initialState, action) => {
  switch(action.type){
    case 'INCREMENT':
      return {...state, count: state.count + 1 }
    case 'DECREMENT'
      return {...state, count: state.count - 1 }
    default:
      return state
  }
}

// app.js - How the app is brought together
import {createStore} from './my-redux'
import {reducer} from './reducer'
import {YourComponent} from './your-component'

const store = createStore(reducer)

YourComponent({
  mount: 'somewhere',
  onmount(){
    store.subscribe(state => {
      this.setYourState(state)
    })
  },
  render(state) {
    return (<button onclick="store.dispatch({ type: 'STH', data: 'NOT VALID JSX BUT WHATEVER' })">Click Me</button>)
  }
})

You can read more about the three principles of Redux and how it's just a guiding concept rather than an actual implementation.

\$\endgroup\$
  • \$\begingroup\$ This clarified much for me. A few questions: 1) why is the initial state set as a default in the reducer? Seems it would be better to pass it in as a second argument to createStore, where it can initialize currentState to initialState. 2) is there an advantage to using a switch block over an if/else, or is it more a conventional choice? 3) What's the best way to extend the possible type of actions? Is it alright to have a 'load-balancing' reducer invoking more specific sub-reducers which perform the state update? \$\endgroup\$ – Jacob Penney Apr 12 '18 at 14:20
  • 1
    \$\begingroup\$ @JacobPenney 1) You can initialize it from createStore if you have some initial state to hydrate the store. But without it, the reducer will at least give you a known, default state shape 2) It's preference. 3) Reducers will always get called to construct the state tree, there's no selective execution. It either constructs new state if it hits a certain action or returns the state it was provided if it has nothing to do with the action. \$\endgroup\$ – Joseph Apr 12 '18 at 15:20
2
\$\begingroup\$

Object.assign({}, model) is not deepcopy, it is shalow copy only, you should consider to use Immutablejs by Facebook or write your own library.

action.type === 'UPDATE_PAGE' ? 
      UPDATE_PAGE(model, action.page)
      : null;

Can be write:

action.type === 'UPDATE_PAGE' && 
      UPDATE_PAGE(model, action.page)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.