2

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.

Community
  • 1
  • 1
bdmason
  • 559
  • 5
  • 14
  • On further inspection, when I use debugger in Chrome, previous state appears as expected, so I guess this may be a console log issue – bdmason Jan 29 '17 at 14:37

2 Answers2

1

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
Pineda
  • 7,435
  • 3
  • 30
  • 45
0

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 })}
/>
Oscar Franco
  • 5,691
  • 5
  • 34
  • 56
  • I'm passing a function to my onChange handler on the input tag, the function is passed down through mapDispatchToProps, effectively doing exactly as your 2nd example shows. I've since used Chrome debugger (using a debugger statement right above the console.log line) and it's showing the previous state as expected, unlike the console.log. It appears everything is working correctly, except console.log! – bdmason Jan 29 '17 at 14:44
  • would you mind updating your question with the code? – Oscar Franco Jan 29 '17 at 14:46