take away the huge switch block #883

Closed
yeatszhang opened this Issue Oct 12, 2015 · 13 comments

Comments

8 participants
@yeatszhang

I really hate the huge switch, so I write a util function to help write reducer in a better way. ( My English is not that good, but I will try to show my idea. )

example

we can write like this.

import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes';

const initialState = [{
  text: 'Use Redux',
  completed: false,
  id: 0
}];

export default createReducer({
  [ADD_TODO]: (state, { text }) => [{
    id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
    completed: false,
    text
  }, ...state],

  [DELETE_TODO]: (state, { id }) => state.filter(todo =>
    todo.id !== action.id
  ),

  [EDIT_TODO]: (state, { id, text }) => state.map(todo =>
    todo.id === id ?
      Object.assign({}, todo, { text }) :
      todo
  ),

  [COMPLETE_ALL]: state => {
    const areAllMarked = state.every(todo => todo.completed);
    return state.map(todo => Object.assign({}, todo, {
      completed: !areAllMarked
    }));
  },

  [CLEAR_COMPLETED]: state => state.filter(todo => todo.completed === false)
}, initialState)

source code

/**
 * an elegance way to write reducer
 * @param funcMap the functions map
 * @param initState initiate state
 * @returns {Function}
 */
function createReducer(funcMap, initialState) {
  if (!isPlainObject(funcMap)) {
    throw new Error('funcMap need to be a plain object');
  }

  return (state = initState, action) =>  map.hasOwnProperty(action.type) ? 
  funcMap[action.type](state, action) : 
  state;
}

features

  • simple, whether use or not depends on you.
  • won't worry about the variable name duplicated
  • can use destruct action and state
  • can use arrow function
  • hashTable is faster than switch when having a lot of cases
  • don't need annoying break and default

@yeatszhang yeatszhang closed this Oct 12, 2015

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Oct 12, 2015

Collaborator

Hi, thanks for sharing! Redux isn't opinionated about how you create the reducers, so indeed you can use any convention you like, including this one. There are cases where you want to match by something other than type though—for example, sometimes you want to respond to action containing a specific field, like action.error or action.response.

Collaborator

gaearon commented Oct 12, 2015

Hi, thanks for sharing! Redux isn't opinionated about how you create the reducers, so indeed you can use any convention you like, including this one. There are cases where you want to match by something other than type though—for example, sometimes you want to respond to action containing a specific field, like action.error or action.response.

@gaearon gaearon added the ecosystem label Oct 12, 2015

@yeatszhang

This comment has been minimized.

Show comment
Hide comment
@yeatszhang

yeatszhang Oct 12, 2015

@johanneslumpe thanks for recommending this useful package. I like the way it simplify actionCreator and reducer. But I think it isn't necessary to use a reduce method to handle multiple actions. Maybe hash is enough and more efficient. But the little problem can't stop me using it ^ ^ . I'd like to have a discussion with the author and contribute to it.

@johanneslumpe thanks for recommending this useful package. I like the way it simplify actionCreator and reducer. But I think it isn't necessary to use a reduce method to handle multiple actions. Maybe hash is enough and more efficient. But the little problem can't stop me using it ^ ^ . I'd like to have a discussion with the author and contribute to it.

@yeatszhang

This comment has been minimized.

Show comment
Hide comment
@yeatszhang

yeatszhang Oct 12, 2015

@gaearon I'm really exciting for your reply cause I really like redux! Your opinion is right, and I found many packages have already actualized my idea. Even I found the same code in redux docs. haha~
My compony use redux from beta version and it works well. Wish redux be better~!

@gaearon I'm really exciting for your reply cause I really like redux! Your opinion is right, and I found many packages have already actualized my idea. Even I found the same code in redux docs. haha~
My compony use redux from beta version and it works well. Wish redux be better~!

@mheiber

This comment has been minimized.

Show comment
Hide comment
@mheiber

mheiber Jan 4, 2016

@yeatszhang here is another way of avoiding switch which doesn't require a reducer factory:

const reducer = (state, action) => {

    const {amount} = action.payload;

    const handlers = {
        [INCREMENT]: () => state + amount,
        [DECREMENT]: () => state - amount
    };

    return handlers[action.type](amount);
};

Advantages are that you can effectively switch on whatever you like, and you can reuse variables defined above the handlers as needed.

mheiber commented Jan 4, 2016

@yeatszhang here is another way of avoiding switch which doesn't require a reducer factory:

const reducer = (state, action) => {

    const {amount} = action.payload;

    const handlers = {
        [INCREMENT]: () => state + amount,
        [DECREMENT]: () => state - amount
    };

    return handlers[action.type](amount);
};

Advantages are that you can effectively switch on whatever you like, and you can reuse variables defined above the handlers as needed.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 4, 2016

Collaborator

There are a few problems there though: allocating functions on every call and not handling the "unknown action" case.

Collaborator

gaearon commented Jan 4, 2016

There are a few problems there though: allocating functions on every call and not handling the "unknown action" case.

@mheiber

This comment has been minimized.

Show comment
Hide comment
@mheiber

mheiber Jan 6, 2016

@gaearon Good points. I guess switch isn't so bad. Thanks for your help!

mheiber commented Jan 6, 2016

@gaearon Good points. I guess switch isn't so bad. Thanks for your help!

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 6, 2016

Collaborator

It's not :-) People tend to obsess over unimportant details, and the disdain for switch seems like one of such cases.

Collaborator

gaearon commented Jan 6, 2016

It's not :-) People tend to obsess over unimportant details, and the disdain for switch seems like one of such cases.

@joaomilho

This comment has been minimized.

Show comment
Hide comment

Another alternative is http://ramdajs.com/0.21.0/docs/#cond

@fracalo

This comment has been minimized.

Show comment
Hide comment
@fracalo

fracalo Oct 12, 2016

IMHO switch blocks are really legible and are perfect for documentation where you need to learn how a reducer works;
but it's true that sometimes this expressiveness can become a little annoying...
anyway here's an alternative pattern :

const reducer = (state, action) => ({
  [INCREMENT]: state + action.payload,
  [DECREMENT]: state - action.payload
}[action.type] || state)

This looks quite good , only cons is you loose switch case fallback.

fracalo commented Oct 12, 2016

IMHO switch blocks are really legible and are perfect for documentation where you need to learn how a reducer works;
but it's true that sometimes this expressiveness can become a little annoying...
anyway here's an alternative pattern :

const reducer = (state, action) => ({
  [INCREMENT]: state + action.payload,
  [DECREMENT]: state - action.payload
}[action.type] || state)

This looks quite good , only cons is you loose switch case fallback.

@Mistereo

This comment has been minimized.

Show comment
Hide comment
@Mistereo

Mistereo Oct 12, 2016

@fracalo your reducer calculates next states for each action in object (no matter for which it was called), this may lead to performance issues.
Also some of your actions may depend on payload shape and that may lead to another unexpected problems:

const reducer = (state, action) => ({
  [INCREMENT]: state + action.payload,
  [DECREMENT]: state - action.payload,
  [FOO]: state + action.payload.foo.bar,
}[action.type] || state)
reducer(0, { type: INCREMENT, payload: 1  })

(this will throw "Uncaught TypeError: Cannot read property 'bar' of undefined")

@fracalo your reducer calculates next states for each action in object (no matter for which it was called), this may lead to performance issues.
Also some of your actions may depend on payload shape and that may lead to another unexpected problems:

const reducer = (state, action) => ({
  [INCREMENT]: state + action.payload,
  [DECREMENT]: state - action.payload,
  [FOO]: state + action.payload.foo.bar,
}[action.type] || state)
reducer(0, { type: INCREMENT, payload: 1  })

(this will throw "Uncaught TypeError: Cannot read property 'bar' of undefined")

@fracalo

This comment has been minimized.

Show comment
Hide comment
@fracalo

fracalo Oct 12, 2016

@Mistereo you're right, once the object is initialized the properties get evaluated so depending on the calculations you need to do it can be way more expensive.

You could make methods out of each property:

const reducer = (state, action) => {
  let o = {
    [INCREMENT]: () => state + action.payload,
    [DECREMENT]: () => state - action.payload,
    [FOO]: () => state + action.payload.foo.bar,
  }[action.type]
  return o ? o() : state
}

but it's starting to get a little noisy.

fracalo commented Oct 12, 2016

@Mistereo you're right, once the object is initialized the properties get evaluated so depending on the calculations you need to do it can be way more expensive.

You could make methods out of each property:

const reducer = (state, action) => {
  let o = {
    [INCREMENT]: () => state + action.payload,
    [DECREMENT]: () => state - action.payload,
    [FOO]: () => state + action.payload.foo.bar,
  }[action.type]
  return o ? o() : state
}

but it's starting to get a little noisy.

@ecancino

This comment has been minimized.

Show comment
Hide comment
@ecancino

ecancino Jul 13, 2017

@fracalo You can always create a helper, this is the one that I use: createReducer

const { createStore, combineReducers  } = require('redux')
const { prop, identity, defaultTo, add, subtract, concat, always } = require('ramda')

const createReducer = (actions, INITIAL) => 
    (state = INITIAL, { type, payload }) => 
        defaultTo(identity, prop(type, actions))(state, payload)

const count = createReducer({ INCREMENT: add, DECREMENT: subtract }, 0)
const list = createReducer({ PUSH: concat, RESET: always([]) }, [])

const { subscribe, dispatch, getState } = createStore(combineReducers({ count, list }))

subscribe(() => console.log(getState()))

dispatch({ type: 'PUSH', payload: [1, 2, 3] })
dispatch({ type: 'DECREMENT', payload: 10 })

ecancino commented Jul 13, 2017

@fracalo You can always create a helper, this is the one that I use: createReducer

const { createStore, combineReducers  } = require('redux')
const { prop, identity, defaultTo, add, subtract, concat, always } = require('ramda')

const createReducer = (actions, INITIAL) => 
    (state = INITIAL, { type, payload }) => 
        defaultTo(identity, prop(type, actions))(state, payload)

const count = createReducer({ INCREMENT: add, DECREMENT: subtract }, 0)
const list = createReducer({ PUSH: concat, RESET: always([]) }, [])

const { subscribe, dispatch, getState } = createStore(combineReducers({ count, list }))

subscribe(() => console.log(getState()))

dispatch({ type: 'PUSH', payload: [1, 2, 3] })
dispatch({ type: 'DECREMENT', payload: 10 })

@motifej motifej referenced this issue in Nortsova/react_redux Sep 16, 2017

Closed

try to use without SWITCH #9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment