1
\$\begingroup\$

I’m using Redux and thunk, I have a list component

class List extends Component {
  render() {

    //code
  }
  return(
    <Aux>
      <Button
        type="button"
        click={() => this.props.addNew(data)}
        btnType="add" >
        Add
      </Button>
      <Button
        type="button"
        click={() => this.props.deleteB(id, type)}
        btnType="Delete" >
        Delete
      </Button>
      <Button
        type="button"
        click={() => this.props.editStart(id)}
        btnType="edit" >
        Edit
      </Button>
      <Button
        type="button"
        click={() => this.props.editSave(data, type)}
        btnType="save" >
        save
      </Button>
    </Aux>
  )
}

const mapStateToProps = state => {
    return {
        editing: state.list.editing
    };
};

const mapDispatchtoProps = dispatch => {
  return {
    deleteB: (id, type) => {dispatch(actions.deleteB(id, type))},
    editStart: (id) => {dispatch(actions.editStart(id))},
    editSave: (data, type) => {dispatch(actions.editSave(data, type))},
    addNew: (data) => dispatch(actions.addNew(data))
  };
};

export default connect(mapStateToProps, mapDispatchtoProps)(List);

A reducer

const addNew = ( state, action ) => {
  //immutable state
  //add new
  //return updated state
};

const deleteB = ( state, action ) => {
  //immutable state
  //delete
  //return updated state
};

const editStart = ( state, action ) => {
  //immutable state
  //update editing to true
  //return updated state
};

const editSave = ( state, action ) => {
  //immutable state
  if(action.type === state.editing.type) {
    //update object with user data
  } else {
    //delete old data same code of deleteB
    //add data user like addNew
    //update editing to false
  }
  //return updated state
};

const reducer = ( state = initialState, action ) => {
  //switch case
};

export default reducer;

When the user clicks on the “save” button if the type of the data changed the reducer uses the same functions that the buttons “add” and “delete” uses. I’ve created an utility function for deleting and adding data, but It still looks ugly.

I was wondering if when the user clicks “Save” it is better to call a function in the List component that calls “addNew” and “deleteB” and finally “editSave” to only update the state for the “editing” state property.

I think that a reducer needs to know only the state he needs to update, so editSave should only update the editing slice of state, and I need to reuse the others reducers. But I don’t know if there is a better way, or if I wrote a bad pattern for the reducers.

\$\endgroup\$
2
\$\begingroup\$

I was wondering if when the user clicks “Save” it is better to call a function in the List component that calls “addNew” and “deleteB” and finally “editSave” to only update the state for the “editing” state property.

I would avoid this. Ideally, all your logic should live either the reducer or the thunk, NOT in components. This way, your logic is in one place, are easily and uniformly testable, and are not affected by things such as re-implementation of the UI (i.e. changing of components, replacing the UI library, etc).

Reducers are just pure functions - they take in old state and an action, and they return new state. As long as you follow that basic principle, you're good. When you call a reducer from another reducer, you're effectively just composing reducers - which isn't a strange concept in Redux. So it's not weird to have something like this:

export const add = (state, action) => { ... }

export const edit = (state, action) => { ... }

export const delete = (state, action) => { ... }

export const someWeirdCaseOfAddEditDelete = (state, action) => {
  if(action.type === state.editing.type) {
    return edit(state, action)
  } else {
    const intermediateState = add(delete(state, action), action)
    const finalState = doMoreStuffWith(intermediateState, action)
    return finalState
  }
}

As an added bonus, when the time comes when someWeirdCaseOfAddEditDelete starts to deviate from your regular add, edit and delete, you can simply replace its implementation and its tests without having to meddle with the other three reducers.


To address your concerns in the comments:

I thought that was an anti-pattern because it's like dispatching an action inside a reducer that got executed from an action.

Calling dispatch in a reducer is the antipattern. But composing functions (i.e. breaking up your reducer into subfunctions that deal with specific parts of the state) is totally fine.

It may be easier to wrap your head around the idea by dropping the assumption that the common operation is a reducer. Think of it as just a common utility function:

const thatCommonFunction = (somePartOfTheState) => ({
  thatOnePropertyYouNeedToChange: { ... }
})

const reducer1 = (state, action) => ({
  ...state,
  ...thatCommonFunction(state.some.part)
})

const reducer2 = (state, action) => ({
  ...state,
  ...thatCommonFunction(state.some.part)
})

Every time I call "deleteB" or "add" from "someWeirdCaseOfAddEditDelete" the state will update, before returning the intended "finalState".

If the state updates before all of your reducers return the new state to Redux, then there's something wrong with your code.

Redux receives the updated state only after it runs through all the reducers. The state should never update while execution is still in the reducer.

Under the hood, at the very basic, Redux does something like this:

const createStore = (rootReducer, initialState = {}) => {
  let currentState = initialState;
  const subscriptions = [];

  return {
    dispatch(action) {
      // This part is where control is handed over to your reducers.
      currentState = rootReducer(currentState, action);

      // Only after the line above should your state be updated.

      // After that one line, the UI is informed of the update.
      subscriptions.forEach(s => s(currentState))
    },
    subscribe(fn) {
      subscriptions.push(fn)
    }
  })
}
\$\endgroup\$
  • \$\begingroup\$ I was first going for this solution, but I thought that was an anti-pattern because it's like dispatching an action inside a reducer that got executed from an action. How the state will behave in this situation? Every time I call "deleteB" or "add" from "someWeirdCaseOfAddEditDelete" the state will update, before returning the intended "finalState". \$\endgroup\$ – AskaNor_29 Jan 26 at 16:03
  • \$\begingroup\$ @AskaNor_29 "Every time I call "deleteB" or "add" from "someWeirdCaseOfAddEditDelete" the state will update, before returning the intended "finalState"." - there's the confusion. I'll update the answer to address this since it's too long for the comment box. \$\endgroup\$ – Joseph Jan 28 at 14:09
  • \$\begingroup\$ Thank you for the clarification. Yes, I was confused, I was not sure if the "common operator" could be called inside another common operator. I will test this solution. \$\endgroup\$ – AskaNor_29 Jan 28 at 18:04

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.