React setting state in useState hook

北城以北 提交于 2020-08-27 21:45:19

问题


I have problem with updating my state using useState hook.
So in my "App" component, I declare my array of objects state:

const [panelSettings, setPanelSettings] = useState([
{
  title: 'Background',
  active: true,
  options: [],
  updateDaily: true
},
{
  title: 'Time and Date',
  active: true,
  showSeconds: true
},
{
  title: 'Weather Forecast',
  active: false,
  city: null
}])

Then I pass {panelSettings} and {setPanelSettings} down to another component, let's call it "Menu".
In this "Menu" component I render each title and have a checkbox next to them, which should set the "active" property. Like so:

{panelSettings.map(element => {
   return (
     <div key={element.title}>
       {element.title}
       <input type='checkbox' checked={element.active} onChange={() => setPanelSettings(element.active = !element.active)} />
     </div>)
})}

But when I click on any of the checkboxes, I get the error "TypeError: Cannot read property 'active' of undefined". However, it comes from my parent component ("App") and not from the "Menu".
I've tried multiple ways to render out the elements and calling the setPanelSettings function but nothing has worked so far. I've also logged out the object from the "Menu" component and it seemed like the 'active' property has changed there.


回答1:


When you do

setPanelSettings(element.active = !element.active)

you are changing panelSettings from

[{
  title: 'Background',
  active: true,
  options: [],
  updateDaily: true
},
{
  title: 'Time and Date',
  active: true,
  showSeconds: true
},
{
  title: 'Weather Forecast',
  active: false,
  city: null
}]

to either true or false. Clearly not what you want.

Probably you want to do:

setPanelSettings((prevState) => prevState.map((s) => {
    if (s === element) {
        return { ...s, active: !s.active };
    }
    return s;
}))



回答2:


Where exactly do you get the error? Is it from the onChange function or from the checked attribute? If it is in the onChange function, you have to use the previous state to update the new state, so it should be something like this:

setPanelSettings((prevState) => {
   return prevState.map((menuInput) => {
       if (menuInput.title === element.title)
          return { ...menuInput, active: !menuInput.active };

       return menuInput;
   });
})



回答3:


Problem:

You are not updating the state correctly.

You are passing a value of an assignment expression to setPanelSettings function. This

element.active = !element.active

is an assignment expression and the value of the assignment expression is the value of the right-side operand, in your case, its the value of !element.active which will either evaluate to true or false. So you are just passing true or false to setPanelSettings function which will change your state from an array of objects to just a boolean value.

Solution:

I would like to suggest a different approach to update the state.

Instead of passing setPanelSettings function to Menu component, create a callback function in App component and pass that to Menu component.

In Menu component, while mapping over panelSettings, use the index parameter to set a data-index attribute on the input element. Whenever any checkbox is checked, callback function passed from App component is called. Callback function then gets the index from the data-index attribute added on the input element. Then it maps over the panelSettings and updates the active property of the object whose index is equal to the data-index attribute of the input element on which onChange event was triggered.

Example:

function Menu({ panelSettings, handleClick }) {
  return (
    <div>
      {panelSettings.map((element, index) => {
        return (
          <div key={element.title}>
            {element.title}
            <input
              type="checkbox"
              checked={element.active}
              onChange={handleClick}
              data-index={index}
            />
          </div>
        );
      })}
    </div>
  );
}

function App() {
  const [panelSettings, setPanelSettings] = React.useState([
    { title: "Background", active: true, options: [], updateDaily: true },
    { title: "Time and Date", active: true, showSeconds: true },
    { title: "Weather Forecast", active: false, city: null }
  ]);

  const handleClick = React.useCallback((event) => {
    const index = event.target.dataset.index;

    const updatedSettings = panelSettings.map((el, idx) =>
      idx == index ? { ...el, active: !el.active } : el
    );

    setPanelSettings(updatedSettings);
  }, [panelSettings]);

  return (
    <div className="App">
      <Menu panelSettings={panelSettings} handleClick={handleClick} />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

You can also view the demo on Codesandbox:

P.S. handleClick in App component is wrapped in useCallback hook to prevent making a new function each time App component re-renders.




回答4:


The comments put me on the right track, and the final solution was this:

{panelSettings.map(element => {
  return (
    <div key={element.title}>
      {element.title}
      <input type='checkbox' checked={element.active} onChange={() => {
        const next = [...panelSettings];
        next[index].active = !next[index].active;
        setPanelSettings(next);
      } />
    </div>)
})}

I'm sure it's not the most sophisticated, but I don't really know how it could be solved in a more concise way and it got the job done. So thanks everyone for the answers.



来源:https://stackoverflow.com/questions/63345213/react-setting-state-in-usestate-hook

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!