React & Redux: State of component is not updating even with mapStateToProps

2.5k views Asked by At

I posted an answer below, but if someone can explain why this is necessary you'll get the bounty, I went through a redux tutorial and feel like I didn't learn about mapDispatchToProps, only mapStateToProps. If you can explain at a deeper level what exactly mapStateToProps and mapDispatchToProps are doing and how they are different I'll give you the bounty.


Minimum Reproducible Example


I have a mapStateToProps function that looks like

const mapStateToProps = state => {
  return {
    firstName: state.firstName,
    middleName: state.middleName,
    lastName: state.lastName,
  }
}

const ReduxTabForm = connect(mapStateToProps)(MyTab)

In my MyTab component I have a button that is supposed to be inactive if these 2 field do not have anything entered in, but the state of whether or not the button is disabled does not change

function App() {
  const {firstName, lastName} = store.getState().formData

  const isDisabled = () => {
    const {firstName, lastName} = store.getState().form
    const requiredFields = [firstName, lastName]
    alert(requiredFields)
    for(let i = 0; i < requiredFields.length; i=i+1){
      if (!requiredFields[i]){
        return true
      }
    }
    return false
  }

  return (

    <div className="App">
        <div className='bg-light rounded'>
          <div className='px-sm-5 pt-0 px-4 flexCenterCol mx-5'>

            <div>
                <input 
                    type='text'
                    className="form-control"
                    value={store.getState().formData['firstName']}
                    placeholder="First Name"
                    onChange={(e) => {
                        store.dispatch(setFormData({'firstName': e.target.value}))
                    }}
               ></input>
               <input 
                    type='text'
                    className="form-control"
                    value={store.getState().formData['lastName']}
                    placeholder="Last Name"
                    onChange={(e) => {
                        store.dispatch(setFormData({'lastName': e.target.value}))
                    }}
               ></input>

            </div>
            <button
                type="submit"
                disabled={isDisabled()}
            >
                Button
            </button>
        </div>
    </div>
  </div>
 )
}

That alert statement executes on page refresh, but does not execute any time after that when I enter data in. I have checked that the redux state updating and it is. The button will not update though, and the isDisabled function will not run more than once

11

There are 11 answers

0
vahissan On BEST ANSWER

I looked at your reducer code and it looks like this:

...
const reducer = combineReducers({
  formData: formReducer,
})

export default reducer

Which means your redux state structure is like this:

state = {
  formData: {
    firstName: <value>,
    middleName: <value>,
    lastName: <value>,
  }
}

Solution

So, to make your component re-render when the redux state is changed, you need to subscribe to the correct state variables in your mapStateToProps function. Change it to this and will work:

const mapStateToProps = state => {
  return {
    firstName: state.formData.firstName, // Note that I added "formData"
    middleName: state.formData.middleName,
    lastName: state.formData.lastName
  }
}

Couple of side notes:

  1. It's a better to use the props instead of directly accessing the redux store.
  2. For debugging, console.log is preferred over alert. React DevTools and Redux DevTools are even better.
5
Mohammad Faisal On

your state values are passed to your component as props. so if you want to access them inside component you should do something like this

 const {firstName, lastName} = props
2
Sam On

The React redux documentation also explains mapDispatchToProps

If you call your action called setFormData from your redux/actions.js then you will be mapping the action setFormData (which is an object) to your component. The action setFromData replace mapDispatchToProps. This is what the documentation says about mapping actions to your components

If it’s an object full of action creators, each action creator will be turned into a prop function that automatically dispatches its action when called.


To fix your problem, change your connect function call to this.

const ReduxApp = connect(
  setFormData,
  mapStateToProps
)(App)
3
Gabriel Filipake On

I don`t know if this will solve your problems, but maybe it is one of the things below: 1 - As Mohammad Faisal said the correct form of calling props should be

const { firstName, lastName } = props;

2 - Instead of reduxTabForm, maybe you could use this instead:

export default connect(mapStateToProps)(MyTab);

3 - And finally, maybe it is an error in the "isDisabled":

for(let i = 0; i < requiredFields.length; i=i+1){
  if (!requiredFields){
    return false
  }
}

If you look carefully, you can see that you are not checking if there is an error inside requeiredFields, your are looking if that doesnt exist if (!requiredFields), maybe changing the condition to if(!requiredFields[i]) so it check each variable and not if the array doesn`t exists.

Edit: the return condition is correct? Returning False when something doesn`t exists?

2
Niyi Aromokeye On

Your issues is with this code isDisabled()

     const isDisabled = () => {
    const {firstName, lastName} = store.getState().form
    const requiredFields = [firstName, lastName]
    alert(requiredFields)
    for(let i = 0; i < requiredFields.length; i=i+1){
      if (!requiredFields){
        return false
      }
    }
    return true
  }

You are trying to test in loop !requiredFields, an array you created which always return false, even if it doesn't have values. It's a reference type. What you can do now is

const isDisabled = () => {
    const {firstName, lastName} = store.getState().form
    const requiredFields = [firstName, lastName]
    alert(requiredFields)
    for(let i = 0; i < requiredFields.length; i=i+1){
      if (!requiredFields[i]){
        return false
      }
    }
    return true
  }

Your loop will check firstNAme and LastName values if they are undefined or not and test should respond with this.

0
Dipen Shah On

The problem is in the mapStateToProps method. Your code is setting firstNAme and lastName as formData within state object and you are trying to access it from state object directly.

This codesandbox has your project updated with the fix. I didn't fix any other thing in your code so everything is as it is only mapStateToProps should be:

const mapStateToProps = (state) => {
  return {
    firstName: state.formData["firstName"],
    middleName: state.middleName,
    lastName: state.formData["lastName"]
  };
};

and this will fix your issue. mapStateToPropsis kind of a projection function which will project entire store to some object which will only have properties based on requirements of your component.

0
Sindujan Nirmalan On
const ReduxTabForm = connect(mapStateToProps,null)(MyTab)

Try this code snippet, as you are not passing dispatch function to your component. It is better to pass null value. mapdispatchtoProps is the same basic theory of mapStateToProps.You are storing the function inside the store(which usually in actions) and when rendering the component you attach those function in store to your props. After rendering the component you will be able to run the functions which are in store.

0
Piyush Rana On

Redux re-render components only when store state changes. Check if you are only updating store's state property not whole state because redux compare state by comparing their references, so if you are doing something like this inside your reducer:

State.firstName = action.payload.firstName;

return State;

Then change this to this:

return {...State, firstName: action.payload.firstName}

Note: If you unable to grasp that then kindly provide your reducer code too so that I can see how you are updating your store state.

0
Tom On
  1. Looking at your MRE on GitHub, the first thing I want to say is that your button will only have one state and it is the one that the isDisabled() method returns when you refresh the page. This is because the App component it's not getting refresh every time you write on the input fields, so you will never be able to make it change.

  2. you need to subscribe to the correct state variables in your mapStateToProps function. Like this:

const mapStateToProps = state => {
    return {
        firstName: state.formData.firstName,
        middleName: state.formData.middleName,
        lastName: state.formData.lastName
    }
}
  1. So now that you have this right, you have to introduce this props into your component, like this:
     function  App({firstName, lastName}) { ...
  1. Another thing to add, is that your are not initializing the states when you create your reducer in reducer.js. If you don't do this, your initial states for firstName and lastName will be null. Here is how you should do it:
import {combineReducers} from 'redux'
import {SET_FORM_DATA} from './actions'

const initialState = {
  firstName: "",
  lastName: ""
}

const formReducer = (state = initialState, action) => {
  if (action.type === SET_FORM_DATA){
    return {
      ...state,
      ...action.payload
    }
  }else{
    return state
  }
}

const reducer = combineReducers({
  formData: formReducer,
})

export default reducer
  1. Finally you have to update App:
function  App({firstName, lastName}) {
  return (
      <div className="App">
        <div className='bg-light rounded'>
          <div className='px-sm-5 pt-0 px-4 flexCenterCol mx-5'>

            <div>
            <input 
              type='text'
              className="form-control"
              placeholder="First Name"
              onChange={(e) => {
                store.dispatch(setFormData({'firstName': e.target.value}));
                console.log(firstName === "h");
              }}
            ></input>
            <input 
              type='text'
              className="form-control"
              placeholder="Last Name"
              onChange={(e) => {
                store.dispatch(setFormData({'lastName': e.target.value}))
                alert(JSON.stringify(store.getState()))

              }}
            ></input>

            </div>
            <button
              type="submit"
              disabled={firstName == "" || lastName == ""}
            >
              Button
            </button>
          </div>
        </div>
      </div>
  );
}

So in this way you will be able to update the states from your store and have the dynamic behavior that you were looking for.

0
Adam Sawicki On

In isDisabled function you read data from form: const {firstName, lastName} = store.getState().form but I guess they are saved to formData.

Changing const {firstName, lastName} = store.getState().form to const {firstName, lastName} = store.getState().formData should help.

3
A Zarqam On

What you could do is something like:

<button 
      type="submit"
      disabled={!fistname && !lastname}
    />

this way if any of your fields be falsy, button is disabled. (empty string is falsy)