bdmason
bdmason

Reputation: 569

Redux previous state already has new state value

I'm trying to get to grips with controlled forms using React & Redux, and I've got it working so that when I'm typing in the input field the state is updating and passing down to the input component as intended.

However, in my reducer, when I console log the previous state, the form field's value doesn't contain the value from before the new character was typed, it already has the new character.

My reducer:

import initialState from '../state/form'
const form = (prevState = initialState, action) => {

  switch (action.type) {

    case 'INPUT': {
      console.log(prevState) // the value here equals "test"
      debugger // the value here equals "tes"
      let newFields = prevState.fields
      newFields[action.field].value = action.value
      return Object.assign({}, prevState, {
        fields: newFields
      })
    }

    default: {
      return prevState
    }
  }
}

export default form

If my input field contains the text "tes", I can then add a "t" and the action is dispatched as intended, but when it gets to this reducer, I console log the previous state and the field's value is "test", not "tes".

I'm expecting the previous state to have "tes", and the reducer to return the new state with "test".

In my container I have:

const dispatchToProps = (dispatch, ownProps) => {
  return {
    control: (e) => {
      dispatch({
        type: 'INPUT',
        form: ownProps.formId,
        field: e.target.getAttribute('name'),
        value: e.target.value
      })
    },
    clear: () => {
      dispatch({
        type: 'CLEAR_FORM',
        form: ownProps.formId
      })
    }
  }
}

So my input component is being passed the 'control' function. I've since used a debugger statement right next to the console.log in the reducer code above, and using Chrome's dev tools, this show prevState to have exactly what I expected (tes, not test). The console.log is still logging "test" though!

So it appears my redux implementation may be ok, there's just some voodoo somewhere as console.log(prevState) == "test" and the debugger allows me to watch the prevState variable and shows that it equals "tes", as expected!

Thanks for your answer @Pineda. When looking into bizarre console log behaviour (as you were typing your answer) I came across the variables are references to objects fact (here) - I've stopped mutating my state, and updated my reducer:

import initialState from '../state/form'
const form = (state = initialState, action) => {
  switch (action.type) {

    case 'INPUT': {
      return Object.assign({}, state, {
        fields: {
          ...state.fields,
          [action.field]: {
            ...state.fields[action.field],
            value: action.value
          }
        }
      })
    }

    default: {
      return state
    }
  }
}

and now it's all working correctly. I may have been appearing to get away with mutating state due to errors in my mapStateToProps method, which had to be resolved for the new reducer to work correctly.

Upvotes: 2

Views: 6077

Answers (2)

Pineda
Pineda

Reputation: 7593

You are mutating state in these lines:

  let newFields = prevState.fields
  newFields[action.field].value = action.value
  // it's also worth noting that you're trying to access a 'value'
  // property on newFields[action.field], which doesn't look
  // like it'll exist

Which can be re-written as:

  prevState.fields[action.field] = action value

You then use your mutated state to create a new object.

Solution:

import initialState from '../state/form'
const form = (prevState = initialState, action) => {

  switch (action.type) {

    case 'INPUT': {
      console.log(prevState);
      // You create a new un-mutated state here
      // With a property 'fields' which is an object
      // with property name whose value is action.field
      // which has the value action.value assigned to it
      const newState = Object.assign({}, prevState, {
        fields: { [action.field]: action.value}
      });
      return 
    }

    default: {
      return prevState
    }
  }
}

export default form

Upvotes: 1

Oscar Franco
Oscar Franco

Reputation: 6250

I'm guessing you are binding your input directly to your redux store attribute:

<input
  value={this.props.name}
  onChange={e => this.props.name = e.target.value}
/>

Remember, values are passed by reference and not by value, if you modify your store value directly then when the action fires you will have already mutated your redux store state (and this is a big no no)

My suggestion is, try to find how are you passing this state around in your codebase, you should have something like:

<input
  value={this.props.name}
  onChange={e => dispatch({type: 'INPUT', field: 'name', value: e.target.value })}
/>

Upvotes: 0

Related Questions