EP143. “Quiz Block 交互逻辑:数组类型 Attribute 与不可变更新”
🔒 登录后可标记已读把静态骨架变成真正能用的多选题编辑器: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