问题
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