Typescript ReactJS:如何向嵌套在对象内的映射数组添加值?

huangapple go评论82阅读模式
英文:

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 &quot;react&quot;;
interface Stress {
label: string
boxes: boolean[]
}
const Stress: React.FC = () =&gt; {
const [stress, setStress] = useState&lt;Array&lt;any&gt;&gt;([{
label: &quot;&quot;,
boxes: [false]
}]);
const [edit, isEdit] = useState&lt;boolean&gt;(false);
return (
&lt;div className=&quot;characterSheetBox&quot;&gt;
&lt;h1&gt;STRESS&lt;/h1&gt;
&lt;button className=&quot;characterSheetButton&quot; onClick={() =&gt; setStress([...stress, { label: &quot;&quot;, boxes: [false] }])}&gt;+blocks&lt;/button&gt;
&lt;button className=&quot;characterSheetButton&quot; onClick={() =&gt; isEdit(!edit)}&gt;Edit&lt;/button&gt;
&lt;div&gt;
{edit ?
stress.map((key: Stress) =&gt; (
&lt;div&gt;
&lt;input type=&quot;text&quot; value={ key.label } /&gt;
{key.boxes.map((box: boolean) =&gt; (
&lt;div&gt;
&lt;svg&gt;
&lt;rect className=&quot;stress&quot; style={{ fill: box ? &quot;red&quot; : &quot;white&quot; }} height={25} width={25} onClick={() =&gt; setStress(!box)} /&gt;
&lt;/svg&gt;
&lt;/div&gt;
))}
&lt;button onClick={() =&gt; setStress([...key.boxes, false])}&gt;+block&lt;/button&gt;
&lt;/div&gt;
))
:
stress.map(key =&gt; (
&lt;div&gt;
&lt;p&gt;{ key.label }&lt;/p&gt;
{ key.boxes }
&lt;/div&gt;
))
}
&lt;/div&gt;
&lt;/div&gt;
)
}
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&lt;Array&lt;any&gt;&gt;([
{
label: &#39;&#39;,
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&lt;any&gt; you can make it Array&lt;Stress&gt; 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&lt;readonly Stress[]&gt;([
{
label: &#39;&#39;,
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.

&lt;svg&gt;
&lt;rect
className=&quot;stress&quot;
style={{ fill: box ? &#39;red&#39; : &#39;white&#39; }}
height={25}
width={25}
onClick={() =&gt; setStress(!box)}
/&gt;
&lt;/svg&gt;

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

  1. Get current state
  2. Map over stress items
  3. If item is not current item return it as it
  4. If item is current map over boxes
  5. 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
) =&gt;
//map over current stress state
current.map((stress, localStressIndex) =&gt; {
//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) =&gt; {
//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

&lt;button onClick={() =&gt; setStress([...key.boxes, false])}&gt;

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

&lt;button
onClick={() =&gt;
setStressData((current) =&gt;
current.map((stress, index) =&gt;
index === stressIndex
? { ...stress, boxes: [...stress.boxes, false] }
: stress
)
)
}
&gt;

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

huangapple
  • 本文由 发表于 2023年6月5日 01:01:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76401504.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定