英文:
Typescript ReactJS: How do you add a value to a mapped array that is nested inside an object?
问题
我正在尝试在我的应力状态中映射"boxes"属性,其中每个布尔值由一个正方形的SVG表示。当网页首次呈现时,它正确显示初始的布尔值集合,但每当我尝试使用"+block"按钮添加新的正方形时,屏幕会显示错误消息:"TypeError: key.boxes is undefined"。我知道"key"和"boxes"都有类型,所以我不明白为什么会出现这个错误。我该如何防止这个错误发生,以便我可以使用"+block"按钮向"boxes"属性添加新的布尔值?
import { useState } from "react";
interface Stress {
label: string;
boxes: boolean[];
}
const Stress: React.FC = () => {
const [stress, setStress] = useState<Array<any>>([{
label: "",
boxes: [false]
}]);
const [edit, isEdit] = useState<boolean>(false);
return (
<div className="characterSheetBox">
<h1>STRESS</h1>
<button className="characterSheetButton" onClick={() => setStress([...stress, { label: "", boxes: [false] }])}>+blocks</button>
<button className="characterSheetButton" onClick={() => isEdit(!edit)}>Edit</button>
<div>
{edit ?
stress.map((key: Stress) => (
<div>
<input type="text" value={key.label} />
{key.boxes.map((box: boolean) => (
<div>
<svg>
<rect className="stress" style={{ fill: box ? "red" : "white" }} height={25} width={25} onClick={() => setStress(!box)} />
</svg>
</div>
))}
<button onClick={() => setStress([...key.boxes, false])}>+block</button>
</div>
))
:
stress.map(key => (
<div>
<p>{key.label}</p>
{key.boxes}
</div>
))
}
</div>
</div>
);
}
export default Stress;
英文:
I am trying to map the boxes property inside my stress state, with each boolean value inside it being represented by an SVG of a square. It correctly displays the initial set of boolean values when the web page first renders, but whenever I try to add a new square using the +block
button, the screen displays: TypeError: key.boxes is undefined
. I know both key
and boxes
have types, so I don't understand why I am getting this error. How would I prevent this error from occurring so I can add a new boolean value to the boxes property using the +block
button?
import { useState } from "react";
interface Stress {
label: string
boxes: boolean[]
}
const Stress: React.FC = () => {
const [stress, setStress] = useState<Array<any>>([{
label: "",
boxes: [false]
}]);
const [edit, isEdit] = useState<boolean>(false);
return (
<div className="characterSheetBox">
<h1>STRESS</h1>
<button className="characterSheetButton" onClick={() => setStress([...stress, { label: "", boxes: [false] }])}>+blocks</button>
<button className="characterSheetButton" onClick={() => isEdit(!edit)}>Edit</button>
<div>
{edit ?
stress.map((key: Stress) => (
<div>
<input type="text" value={ key.label } />
{key.boxes.map((box: boolean) => (
<div>
<svg>
<rect className="stress" style={{ fill: box ? "red" : "white" }} height={25} width={25} onClick={() => setStress(!box)} />
</svg>
</div>
))}
<button onClick={() => setStress([...key.boxes, false])}>+block</button>
</div>
))
:
stress.map(key => (
<div>
<p>{ key.label }</p>
{ key.boxes }
</div>
))
}
</div>
</div>
)
}
export default Stress;
答案1
得分: 1
以下是已翻译的代码部分:
interface Stress {
label: string;
boxes: boolean[];
}
const [stressData, setStressData] = useState<readonly Stress[]>([
{
label: '',
boxes: [false],
},
]);
// ...
<svg>
<rect
className="stress"
style={{ fill: box ? 'red' : 'white' }}
height={25}
width={25}
onClick={() => setStressData((current) =>
current.map((stress, localStressIndex) => {
if (localStressIndex === stressIndex) {
return {
...stress,
boxes: stress.boxes.map((box, localBoxIndex) => {
if (localBoxIndex === boxIndex) {
return !box;
}
return box;
}),
};
}
return stress;
})
)}
/>
</svg>;
// ...
<button
onClick={() =>
setStressData((current) =>
current.map((stress, index) =>
index === stressIndex
? { ...stress, boxes: [...stress.boxes, false] }
: stress
)
)
}
>
希望这有所帮助。
英文:
there's a few things that could be improved in the code that will make it easier to see the issue you're referring to, maybe best to go through it bit by bit.
interface Stress {
label: string;
boxes: boolean[];
}
const [stress, setStress] = useState<Array<any>>([
{
label: '',
boxes: [false],
},
]);
Here you've declared a type for Stress so you can use it for the state which should be an array of Stress. So instead of Array<any>
you can make it Array<Stress>
or even readonly Stress[]
since props and state are to be treated as readonly in react. You probably also want to change the name because stress as array of stress is just gonna be confusing. So say you end up with something like
const [stressData, setStressData] = useState<readonly Stress[]>([
{
label: '',
boxes: [false],
},
]);
Then this already highlights existing issues you had when setting state further down in the code.
Then edit
and isEdit
are confusing names, second member returned from useState
is a setter so isEditing
and setEditing
are clearer.
Within the code that maps the data to jsx elements, and calls to setStressData
target the entire array of data. So any update needs to consider the entirety of the state.
<svg>
<rect
className="stress"
style={{ fill: box ? 'red' : 'white' }}
height={25}
width={25}
onClick={() => setStress(!box)}
/>
</svg>
In this case, if you call setStress
with !box
you've lost the data. Instead of having a state with an array of Stress
objects you have state with a single boolean
value. So whatever argument you pass to setStress
must be of type Stress[]
. So in this case you want to toggle the value for the current box in the current stress, so you need to map over the current state updating the values at that index. So the flow is this
- Get current state
- Map over stress items
- If item is not current item return it as it
- If item is current map over boxes
- If box is current box apply any updates and return updated
So in code it looks like
setStressData(
(
current //using setState callback form can get current as arg
) =>
//map over current stress state
current.map((stress, localStressIndex) => {
//if same index as that being rendered
//stressIndex here is argument provided when mapping over data in jsx
if (localStressIndex === stressIndex) {
//if same return updated
return {
//spread existing values
...stress,
//map over box values
boxes: stress.boxes.map((box, localBoxIndex) => {
//if box index same as current in render
if (localBoxIndex === boxIndex) {
//return updated value
return !box;
}
//return as is
return box;
}),
};
}
//return as is
return stress;
})
);
It seams like a lot but it can be condensed a lot with ternary expressions etc.. But this is almost like a core thing for data management in react. If you split components up into smaller components also gets more organized.
So it's a similar thing for the part
<button onClick={() => setStress([...key.boxes, false])}>
You need to return an array of stress, here the issue is you're passing key.boxes
which is boolean[]
for current stress. You need to do the same, so ends up being
<button
onClick={() =>
setStressData((current) =>
current.map((stress, index) =>
index === stressIndex
? { ...stress, boxes: [...stress.boxes, false] }
: stress
)
)
}
>
So that's similar to what we did above to map over the box values but a bit simpler as it's just adding a box rather than mapping over boxes as well.
For both these cases we used index which can work it just means these can't be reordered in the array. Eventually you can use some id defined on stress object.
Here's a playground link with example https://stackblitz.com/edit/stackblitz-starters-udz66w?file=src%2FApp.tsx
Hope it helps
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论