Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isomorphic states actions #1511

Closed
eloytoro opened this issue Mar 11, 2016 · 6 comments
Closed

Isomorphic states actions #1511

eloytoro opened this issue Mar 11, 2016 · 6 comments

Comments

@eloytoro
Copy link

Usind redux I've realized there are many times where my state structure ends up with very similar sub states that could reuse the same logic applied to similar states.

const initialState = {
  highPriority: {
    todos: []
  },
  lowPriority: {
    todos: []
  }
};

Both of these so-called "sub-states" could use the same logic to add, remove or clear todos from the list. However the naive approach leads to a very unscalable result

const reducerFn = (state, action) => {
  switch (action.type) {
    case ADD_HIGH_PRIORITY_TODO:
      return {
        ...state,
        highPriority: {
          ...state.highPriority,
          todos: [action.text, ...state.highPriority.todos]
        }
      }
    case ADD_LOW_PRIORITY_TODO:
      return {
        /*same for above but using different substate*/
      }
    default: return state;
  }
};

So each substate has its own "same-action" identifier, with its own action creator, and "almost-same" reduce logic.

First step could be delegating the reducing to another reducer

const otherReduceFn = (state, action) {
  switch (action.type) {
    case ADD_HIGH_PRIORITY_TODO:
    case ADD_LOW_PRIORITY_TODO:
      return {
         ...state,
         todos: [action.text, ...state.todos]
       };
    default: return state;
  }
};

const reducerFn = (state, action) => {
  switch (action.type) {
    case ADD_HIGH_PRIORITY_TODO:
      return {
        ...state,
        highPriority: otherReducerFn(state.highPriority, action)
      };
    case ADD_LOW_PRIORITY_TODO:
      return {
        ...state,
        lowPriority: otherReducerFn(state.lowPriority, action)
      };
    default: return state;
  }
};

But this still doesnt cut it, there's a lot of code repetition and merging all action handling into one big ADD_TODO case would cause unavoidable pollution to the action's body in order to differentiate which action changes which part of the state.

So I came up with this solution, adding context to the action creator could allow actions to identify the state they act upon by diving into the state structure with the use of namespaces.

A namespace would be the key of the child state within the parent state

const NAMESPACE = 'namespace';

const initialState = {
  [NAMESPACE]: {
    /* ... */
  }
};

Using a namespace as the context to the dispatch function binds the child state as the state passed on to the reducer

NAMESPACE::dispatch(action) // would use the object under NAMESPACE as the state

Using this idea with the previous example it ends up being like this

const initialState = {
  [HIGH_PRIORITY_NAMESPACE]: {
    todos: []
  },
  [LOW_PRIORITY_NAMESPACE]: {
    todos: []
  }
};

const reducerFn = (state, action) => {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [action.text, ...state.todos]
      }
    default: return state;
  }
}

dispatch(HIGH_PRIORITY_NAMESPACE::addTodo('clean up nested states'))
dispatch(LOW_PRIORITY_NAMESPACE::addTodo('delete merged branches'))

The only reason this isn't fully possible at the moment is because bindActionCreators doesn't allow the resulting wrapped action creators to have context, would this be possible without breaking the current API? Is this context based approach something frowned upon within applications that use redux?

EDIT: changed syntax from NAMESPACE::dispatch(actionCreator()) to dispatch(NAMESPACE::actionCreator())

@gaearon
Copy link
Contributor

gaearon commented Mar 11, 2016

We generally recommend you to normalize your state instead:

{
  todosById: {
    0: { id: 0, text: 'lol' },
    1: { id: 1, text: 'hi' },
    2: { id: 2, text: 'wow' }
  },
  highPriorityIds: [0, 1],
  lowPriorityIds: [2]
}

Then there is no duplication of logic.
Please check examples/shopping-cart for more details on this approach.
See also #994.

Hope this helps!
Feel free to continue the discussion here but I’m closing because it doesn’t appear actionable.

@gaearon gaearon closed this as completed Mar 11, 2016
@eloytoro
Copy link
Author

While the cart example shows some insight on how to manage nested states it still doesn't allow the same action to operate on different but identical states. I would have to define an action creator and an action type for each of the existing actions whereas using something like a namespace would allow me to define upon which part of the state I wish to dispatch an action.

Take this example

const initialState = {
  A: {/*...*/},
  B: {/*...*/},
  C: {/*...*/}
}

A, B and C all have the same state structure, or at least a very similar one, that allows some reducers to apply actions to them. Using the given approach I would have to define an action creator and an action type to tell the reducer to which of the parts of the state it should apply.

const TO_A = 'TO_A'
const TO_B = 'TO_B'
const TO_C = 'TO_C'

const doToA = () => {
  type: TO_A
}

const doToB = () => {
  type: TO_B
}

const doToC = () => {
  type: TO_C
}

const reducer = (state, action) {
  switch action.type {
    case TO_A: return {
      ...state,
      A: commonReducer(state.A, action)
    }
    case TO_B: return {
      ...state,
      B: commonReducer(state.B, action)
    }
    case TO_A: return {
      ...state,
      C: commonReducer(state.C, action)
    }
  }
}

While it could be something like

const DO_ACTION = 'DO_ACTION';

function doAction() {
  type: DO_ACTION,
  namespace: this
}

const reducer = (state, action) {
  switch action.type {
    case DO_ACTION: return {
      ...state,
      [action.namespace]: actionReducer(state[action.namespace], action)
    }
  }
}

// and to dispatch the action
dispatch('A'::doAction())
dispatch('B'::doAction())
dispatch('C'::doAction())

@gaearon
Copy link
Contributor

gaearon commented Mar 11, 2016

This is a little bit too abstract. Can you help me understand what you’re aiming for specifically?

@eloytoro
Copy link
Author

Reusing reducers and action creators across similar states to share logic that applies to them, also allowing to modify any given target inner state without the need of adding more logic to the application.

@gaearon
Copy link
Contributor

gaearon commented Mar 11, 2016

Have you looked at reducers in examples/real-world? We use reducer factories there for that.

@eloytoro
Copy link
Author

Going to reference #822 and #1528 since they do a better job explaining the issue with non-rehusable reducers that operate on top of abstract, isomorphic states.
Clearly this is a very well known architectural issue with redux applications and the examples shown here are nothing but a naive attempt at solving them.

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

No branches or pull requests

2 participants