更改构建组件的参数时,数组状态不会更新 [已解决]

Array state doesn't update when changing the parameters of a built component [Solved]

提问人:Roro Cyr 提问时间:11/10/2023 最后编辑:Roro Cyr 更新时间:11/21/2023 访问量:35

问:

找到了答案,但无法回复我自己的帖子,所以这是一个编辑:

答案是:

确保在更改对象时键会发生变化,并且它会按应有的方式更新内容。

代码的版本要晚得多,但这是我如何让它干净地工作。

setExampleRender(<GapSection {
        ...sectionLookup(exampleIds[curentExampleIndex].exempleId)}
        updateProgress={updateProgress}
        progress={progress}
        mode={modeEnum.Example}
        key={exampleIds[curentExampleIndex].exempleId} />) // <- the key here fixes everything, make sure it is unique when you change your component, that way, React knows when you changed it.

以下是我原封不动的原始问题:

我正在制作一个基本网站的演示,用 NextJS 和 Reactdnd 填补 React 中的空白。

我有一个名为 ExempleFill 的组件,它包含:

text: text of the exercice as an array of strings
blanks: blanks to be filled as an array
options: possible answers as an array

此组件具有一个名为 answers 的状态,它是空格和 answerOptions ID 的元组数组:

const [answers, setAnswers] = useState<correctAnswerType[]>(initiateAnswers(blanks))

export interface correctAnswerType {
    blankId: number;
    answerId: number | null;
  }

当答案设置为空白时,answerId 将更新,并且 FillBlank 组件将传递 ExempleFill 的子组件(也是 ExempleFill 的子组件)中的文本值。

父组件通过以下命令调用它:

<ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('abstract'))[attempts]?.exempleId)}/>

当练习失败时,作为父组件状态的尝试将递增。

发生这种情况时,练习的文本和选项都会发生变化。但是,从 FillAnswer 传递的 FillBlank 中的值不会,answers 数组状态也不会。

然后发生的事情是这样的:

5x | 10x | 8x
5x+3x = __ // original state
5x | __ | 8x
5x+3x = 10x //Wrong inputted answer by user.

然后,当用户执行问题更改时,会建议用户重试:

3y | __ | 5y
9y-4y = 10x //Answer remains from the previous question

预期的行为是让问题变成这样的:

3y | 13y | 5y
9y-4y = __ 

在尝试许多解决方案之前,您将在下面找到原始代码的相关部分。我是一个相当糟糕的开发人员,所以我确信这段代码中有很多错误和非常规的东西。我很乐意对您找到的所有内容提供一些反馈,但请优先考虑我提出的问题。

父组件:

const Topic = (topic: TopicSectionProps): JSX.Element => {
    const {name, exempleIds} = topic
  return (
    <div>
       // Irrelevant code (other divs, title, and extra stuff
       <ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('abstract'))[attempts]?.exempleId)}/>
        </div>
    </div>
  )
}

ExempleFill:

const ExempleFill = (exempleFill: ExempleFillProps): JSX.Element => {
  const {text blanks, options} = exempleFill

  const [answers, setAnswers] = useState<correctAnswerType[]>(initiateAnswers(blanks))

  const answerUpdate = (blankId: number, answerId: number): void => {
    const answersCopy = [...answers]
    answersCopy.map((answer) => {
      if (answer.blankId === blankId) {
        answer.answerId = answerId
      }
    })
    setAnswers(answersCopy)
  }
  
 // irrelevant code to randomise the order the options show up in

  return (
    <div className='mt-4'>
      <div>
        {options.map((option) => <FillAnswer text={option.text} id={option.id} answers={answers} key={option.id} />)}
      </div>
      <div className="mt-4">
        {blanks?.map((blank, index) =>
        <span key={blank.blankId}>
          {text[index]}
          <FillBlank placeholder={blank.placeholder} blankId={blank.blankId} answerUpdate={answerUpdate} />
        </span>
        )}
        {text && text[text?.length-1]}
      </div>
      // irrelevant code
      </div>
    </div>
  )
}

填写答案

const FillAnswer = (answer: FillAnswerProps): JSX.Element => {
  const { text: answerText, id: answerId, answers } = answer

  const [{ isDragging }, drag] = useDrag(() => ({
    type: 'answer',
    item: { answerText, answerId },
    collect(monitor) {
      const isDragging = monitor.isDragging();
      return {
        isDragging,
      };
    },
  }), [answerText, answerId])

  const dropped = answers?.filter(answer => answer.answerId === answerId).length > 0
  
  return (
    <span
      className="border-2 border-white w-fit"
      ref={drag}
      style={{
        visibility: dropped || isDragging ? "hidden" : "visible"
      }}
    >
      {answerText.toString()}
    </span>
  )
}

填写空白

const FillBlank = ({placeholder, blankId, answerUpdate}: FillBlankProps): JSX.Element => {
    const [answer, setAnswer] = useState<string>(placeholder)

    const [{ isOver }, drop] = useDrop(() => ({
        accept: 'answer',
        drop: (item: {answerText: string, answerId: number}) => {
          setAnswer(item.answerText)
          answerUpdate(blankId, item.answerId)
        },
        collect: (monitor) => ({
            isOver: !!monitor.isOver(),
        }),
    }))

  return (
    <span
      className="border-2 border-white w-fit"
      ref={drop}
      >
        {answer}
      </span>
  )
}

我尝试的第一件事是将尝试传递给 ExempleFill 并使用 useEffect 重置答案数组。

const ExempleFill = (exempleFill: ExempleFillProps): JSX.Element => {
  const {text, blanks, options, attempts} = exempleFill

  const [answers, setAnswers] = useState<correctAnswerType[]>(initiateAnswers(blanks))

  const answerUpdate = (blankId: number, answerId: number): void => {
    const answersCopy = [...answers]
    answersCopy.map((answer) => {
      if (answer.blankId === blankId) {
        answer.answerId = answerId
      }
    })
    setAnswers(answersCopy)
  }

  useEffect(() => {
      setAnswers(initiateAnswers(blanks))
    }, [attempts, blanks])

这没有效果

然后,我尝试在主题级别使用useEffect

  const [exempleRender, setExempleRender0] = useState<JSX.Element | undefined>(undefined)

  useEffect(() => {
    setExempleRender(<ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('concrete'))[attempts]?.exempleId)} attempts={attempts} />)
  }, [attempts, exempleIds)

// then later in the output:
{exempleRender ?? <></>}

这也没有用

也尝试过这种方式,认为不同的建筑可以工作

  const [exempleRender, setExempleRender0] = useState<JSX.Element | undefined>(undefined)

  useEffect(() => {
    switch (attempts) {
      case 0:
        setExempleConcreteRender(<ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('concrete'))[0]?.exempleId)} attempts={attempts} />)
        break;
      case 1:
        setExempleConcreteRender(<ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('concrete'))[1]?.exempleId)} attempts={attempts} />)
        break;
    }
  }, [attempts, exempleIds])

// then later in the output:
{exempleRender ?? <></>}

这也失败了。

我找到了一种有效的方法,但即使是我是一个糟糕的开发人员也意识到这绝对是有史以来最糟糕的事情。它包括代码重复,并且没有任何可扩展性。

const [exempleConcreteRender0, setExempleConcreteRender0] = useState<JSX.Element | undefined>(undefined)
const [exempleConcreteRender1, setExempleConcreteRender1] = useState<JSX.Element | undefined>(undefined)

useEffect(() => {
  if (attempts === 0) {
    setExempleConcreteRender0(<ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('concrete'))[0]?.exempleId)} attempts={attempts} />)
  } else {
    setExempleConcreteRender0(<></>)
  }
  if (attempts === 1) {
    setExempleConcreteRender1(<ExempleFill {...exempleFillLookup(exempleIds.filter((id) => id.tags.includes('concrete'))[1]?.exempleId)} attempts={attempts} />)
  } else {
    setExempleConcreteRender1(<></>)
  }
}, [attempts, exempleIds])


// then later in the output
{exempleConcreteRender0 ?? <></>}
{exempleConcreteRender1 ?? <></>}

这完全是垃圾,但有效。它绝对永远不会可扩展,我最终会需要它,请帮助我找到更好的方法来做到这一点。

下一个.js react-hooks react-typescript react-dnd

评论

0赞 Roro Cyr 11/21/2023
我已经找到了答案,但我无法回复该帖子,所以我将其放在评论中。确保在更改对象时键会发生变化,并且它会按应有的方式更新内容。

答: 暂无答案