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;
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
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