WP DEVELOP

EP143. “Quiz Block 交互逻辑:数组类型 Attribute 与不可变更新”

首页 WordPress 开发课程 GUTENBERG BLOCK 入门(block.json) · EP143
约 14 分钟· #EP143#GUTENBERG BLOCK 入门(block.json)
🔒 登录后可标记已读

把静态骨架变成真正能用的多选题编辑器:question 属性存问题文字;answers 属性改成 数组类型,用 .map() 循环渲染出对应数量的答案输入行;实现「输入答案文字」「新增一行」「删除某一行」三个交互,核心难点是 React/WordPress 的属性系统要求不可变更新——不能直接改数组里的某一项或用 push() 追加,必须先用 concat() 复制出一份新数组、在新数组上改,再整个丢给 setAttributes()


涉及文件

  • wp-content/plugins/are-you-paying-attention/src/index.js (修改)

代码实现

attributes 改成真正的问答结构(answers 用数组类型)

attributes: {
  question: {type: "string"},
  answers: {type: "array", default: ["red", "blue", "green"]} // default 这里是调试用的测试值,实际应设为 [""],让页面首次加载时有且只有一行空答案
},

EditComponent:问题输入、答案循环渲染、增删答案的完整逻辑

function EditComponent (props) {

  function updateQuestion(value) {
    props.setAttributes({question: value})
  }

  function deleteAnswer(indexToDelete) {
    const newAnswers = props.attributes.answers.filter(function(x, index) {
      return index != indexToDelete
    })
    props.setAttributes({answers: newAnswers})
  }

  return (
    <div className="paying-attention-edit-block">
      <TextControl label="Question:" value={props.attributes.question} onChange={updateQuestion} style={{fontSize: "20px"}} />
      <p style={{fontSize: "13px", margin: "20px 0 8px 0"}}>Answers:</p>
      {props.attributes.answers.map(function (answer, index) {
        return (
          <Flex>
            <FlexBlock>
              <TextControl value={answer} onChange={newValue => {
                const newAnswers = props.attributes.answers.concat([])
                newAnswers[index] = newValue 
                props.setAttributes({answers: newAnswers})
              }} />
            </FlexBlock>
            <FlexItem>
              <Button>
                <Icon className="mark-as-correct" icon="star-empty" />
              </Button>
            </FlexItem>
            <FlexItem>
              <Button isLink className="attention-delete" onClick={() => deleteAnswer(index)}>Delete</Button>
            </FlexItem>
          </Flex>
        )
      })}
      <Button isPrimary onClick={() => {
        props.setAttributes({answers: props.attributes.answers.concat([""])})
      }}>Add another answer</Button>
    </div>
  )
}

关键改动点:

  • question 属性绑定 TextControl 时,onChange 收到的直接就是新的文本值(不是原生 DOM 的 event 对象)——这是 WordPress 组件相比原生 <input> 更省事的地方,不需要再手动 event.target.value 取值
  • answers: {type: "array", default: [...]}:数组类型属性,default 决定 Block 第一次插入、数据库里还没有值时的初始状态——production 环境应该是 [""](一个空字符串),保证一开始就有一行可以填写,这一讲用 ["red", "blue", "green"] 只是方便边写边测试循环效果
  • {props.attributes.answers.map(function (answer, index) { return (...) })}:JSX 里通过 {} 嵌入一段 JS 表达式,.map() 遍历数组、给每一项返回一段 JSX,最终 React 会把这个「JSX 数组」原样渲染成对应数量的 <Flex> 行——map 回调的第二个参数 index 是当前项在数组里的位置,后面删除/编辑单个答案都要靠这个下标定位
  • 不可变更新(immutable update) 是这一讲反复强调的核心原则:React 里数据(存在 attributes/state 里的值)不能被直接修改,只能「生成一份新的、替换掉旧的」交给 setAttributes()。三处具体做法:
    • 改某一项props.attributes.answers.concat([]) 先复制出一份新数组(concat() 传空数组不会真的拼接什么,只是利用它「返回新数组」的副作用),再对这份新数组的指定下标赋值(newAnswers[index] = newValue),最后把这份新数组整个传给 setAttributes()——绝不能直接改 props.attributes.answers[index]
    • 新增一项:不用会直接修改原数组的 push(),而是用 props.attributes.answers.concat([""])——concat() 天然返回一个新数组(原数组末尾多了一个空字符串),不会动到原数组本身
    • 删除一项:用 .filter() 遍历原数组,只保留「下标不等于要删除的下标」的项,天然生成一份不包含目标项的新数组——同样不会修改原数组
  • 为什么要坚持不可变更新:React(以及 WordPress 用的这套机制)靠「新旧值是不是同一个引用」来判断要不要重新渲染界面;如果直接原地修改原数组,新旧引用其实是同一个对象,React 可能察觉不到数据变了,导致界面不刷新
  • 删除按钮的 onClick={() => deleteAnswer(index)}:这里必须用箭头函数包一层再调用 deleteAnswer(index),而不是直接写 onClick={deleteAnswer(index)}——后者会在渲染的那一刻就立即执行函数(因为带了括号),而不是等用户真正点击时才执行,这是 JSX 里处理「需要带参数的点击回调」的标准写法

[截图:Gutenberg 编辑器里点击 Add another answer 新增出多行答案输入框、再点某一行 Delete 删除该行后的效果]


Hook / Function 速查

名称类型用途
Array.prototype.map(fn)JS 内建方法遍历数组,为每一项返回一个新值,组成新数组(这里用来生成 JSX 元素列表)
Array.prototype.concat(arr)JS 内建方法返回一个新数组,不修改原数组,常用来「安全地」复制/追加
Array.prototype.filter(fn)JS 内建方法遍历数组,只保留回调返回 true 的项,组成新数组,不修改原数组
onClick={() => fn(arg)}JSX 事件绑定写法需要带参数调用回调函数时,用箭头函数包一层,避免渲染时立即执行

常见坑

  • 直接修改 props.attributes.answers[index] = ... 或用 .push()/.splice() 这类会原地改动原数组的方法——即使数据表面上看似改对了,React/WordPress 可能因为引用没变而不会触发界面重新渲染,或者产生更隐蔽的状态不一致问题
  • onClick={deleteAnswer(index)} 忘记用箭头函数包裹——会导致函数在渲染阶段(页面还没被点击)就立即执行一次,而不是等待用户真正点击
  • 数组属性的 default 用了空数组 []——首次加载没有任何一行可填的答案.map() 循环不出任何内容,用户没法从零开始新增

延伸 / 后续讲座会用到

下一讲实现「点星标标记正确答案」的逻辑,用一个额外的属性记录「当前正确答案的下标」。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 25, EP143