WP DEVELOP

EP150. “用 useEffect 实现延迟反馈与答对后锁定”

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

打磨 Quiz 前台的细节体验:答错一次后如果再答错一次,因为 isCorrect 状态值没有变化(falsefalse),React 侦测不到变化、动画不会重新播放——用 useEffect 监听 isCorrect,在动画播完后(2600ms)把它重置回 undefined,这样下次答错才能真正触发一次新的变化。同时实现「答对后禁止再点」和「答对后显示对错图标、错误答案变半透明」的效果——为了避免多个动画同时播放显得杂乱,特意用第二个状态 isCorrectDelayed 让图标/样式变化延迟到提示消息淡入淡出到一半(1000ms)才出现,让「消息淡出」和「答案变样式」两个视觉变化的时间点错开。


涉及文件

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

代码实现

src/frontend.js(完整文件)

import React, {useState, useEffect} from 'react'
import ReactDOM from 'react-dom'
import "./frontend.scss"

const divsToUpdate = document.querySelectorAll(".paying-attention-update-me")

divsToUpdate.forEach(function(div) {
  const data = JSON.parse(div.querySelector("pre").innerHTML)
  ReactDOM.render(<Quiz {...data} />, div)
  div.classList.remove("paying-attention-update-me")
})

function Quiz(props) {
  const [isCorrect, setIsCorrect] = useState(undefined)
  const [isCorrectDelayed, setIsCorrectDelayed] = useState(undefined)

  useEffect(() => {
    if (isCorrect === false) {
      setTimeout(() => {
        setIsCorrect(undefined)
      }, 2600)
    }

    if (isCorrect === true) {
      setTimeout(() => {
        setIsCorrectDelayed(true)
      }, 1000)
    }
  }, [isCorrect])
  
  function handleAnswer(index) {
    if (index == props.correctAnswer) {
      setIsCorrect(true)
    } else {
      setIsCorrect(false)
    }
  }

  return (
    <div className="paying-attention-frontend">
      <p>{props.question}</p>
      <ul>
        {props.answers.map(function(answer, index) {
          return (
          <li className={(isCorrectDelayed === true && index == props.correctAnswer ? "no-click" : "") + (isCorrectDelayed === true && index != props.correctAnswer ? "fade-incorrect": "")} onClick={isCorrect === true ? undefined : () => handleAnswer(index)}>
            {isCorrectDelayed === true && index == props.correctAnswer && (
              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" className="bi bi-check" viewBox="0 0 16 16">
                <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
              </svg>
            )}
            {isCorrectDelayed === true && index != props.correctAnswer && (
              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" className="bi bi-x" viewBox="0 0 16 16">
                <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
              </svg>
            )}
            {answer}
          </li>
          )
        })}
      </ul>
      <div className={"correct-message" + (isCorrect == true ? " correct-message--visible" : "")}>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="bi bi-emoji-smile" viewBox="0 0 16 16">
          <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
          <path d="M4.285 9.567a.5.5 0 0 1 .683.183A3.498 3.498 0 0 0 8 11.5a3.498 3.498 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.498 4.498 0 0 1 8 12.5a4.498 4.498 0 0 1-3.898-2.25.5.5 0 0 1 .183-.683zM7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/>
        </svg>
        <p>That is correct!</p>
      </div>
      <div className={"incorrect-message" + (isCorrect === false ? " correct-message--visible" : "")}>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="bi bi-emoji-frown" viewBox="0 0 16 16">
          <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
          <path d="M4.285 12.433a.5.5 0 0 0 .683-.183A3.498 3.498 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.498 4.498 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683zM7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/>
        </svg>
        <p>Sorry, try again.</p>
      </div>
    </div>
  )
}

关键改动点:

  • 问题背景:答错一次后 isCorrect 变成 false,React 重新渲染、错误提示淡入;如果这时候再点另一个错误答案,handleAnswer() 又把 isCorrect 设成 false——但它本来就已经是 false 了,值没有变化,React(以及依赖它的动画 class)根本不会重新触发,所以第二次点错「看起来没反应」
  • 解法:useEffect(callback, [依赖项])——这是另一个 React Hook,用来在指定的状态发生变化时执行一段「副作用」代码。第二个参数是一个数组,列出要监听的状态;这里只监听 [isCorrect],意味着只要 isCorrect 变化(不管变成什么),这个回调函数就会重新执行一次
  • useEffect 内部逻辑:
    • if (isCorrect === false):答错后用 setTimeout 等 2600 毫秒(跟错误提示的淡入淡出动画时长对齐),再把 isCorrect 重置回 undefined——这样如果用户再次答错,状态是从 undefined 变成 false(真正的变化),动画能重新触发
    • if (isCorrect === true):答对后用 setTimeout 等 1000 毫秒(大约是提示消息动画播放到一半的时间点),才把第二个状态 isCorrectDelayed 设成 true——这个延迟是刻意设计的:如果「提示消息淡出」和「答案列表变样式(图标、半透明)」两个动画同时发生,视觉上会显得杂乱,所以让后者延后出现,正好在提示消息渐渐消失、快要看到答案列表的时候完成变化
  • 新增 isCorrectDelayed 状态:跟 isCorrect 分开管理,专门驱动「点击列表项时是否已锁定」「勾选/叉号图标是否显示」「正确答案不可再点/错误答案半透明」这几个跟点击后样式相关的效果,isCorrect 本身只负责驱动那两条大的提示消息
  • onClick={isCorrect === true ? undefined : () => handleAnswer(index)}:用三元运算符实现「答对之后彻底不能再点任何答案」——如果 isCorrect 已经是 trueonClick 直接传 undefined(等于没有绑定点击事件);否则正常绑定 handleAnswer
  • <li>className 用两段字符串拼接的三元表达式:isCorrectDelayed === true 且是正确答案下标 → 加 "no-click"(改变鼠标样式,不再显示可点击的手型),isCorrectDelayed === true 且是错误答案下标 → 加 "fade-incorrect"(半透明)
  • 图标用 JSX 里 条件 && (<jsx内容>) 这个常见写法——&& 短路运算:条件为真时才会渲染后面括号里的内容,条件为假时 && 整体求值为 false,React 遇到 false 会直接跳过、不渲染任何东西。这里分别放了打勾(bootstrap icons 的 bi-check)和叉号(bi-x)两个 SVG,逻辑跟之前用过的 smile/frown 图标一样,都是从 Bootstrap Icons 网站复制、把 class 改成 className、去掉 fill、调整宽高(这里是 20×20)
  • 列表项原本单行就能写完 JSX,因为现在内容变多、要换行排版,改成用一对括号 (...) 包住多行 JSX——这是 JSX 的标准写法:只要 return 或箭头函数返回值要跨多行,就要用括号包起来,否则 JS 会因为自动分号插入机制在换行处提前结束语句

[截图:前台页面答对题目后的效果,正确答案旁出现打勾图标、其余答案变半透明并出现叉号图标、答案不再可点击]


Hook / Function 速查

名称类型用途
useEffect(callback, [依赖项数组])React Hook在依赖项数组里的状态发生变化时,执行一段副作用逻辑(这里用来在动画结束后重置状态)
setTimeout(callback, 毫秒数)JS 内建函数(浏览器/Node 原生)延迟指定时间后执行一次回调函数
条件 && (<JSX>)JSX 常见写法利用 && 短路特性实现「条件为真才渲染」,条件为假时 React 直接跳过

常见坑

  • 只用一个 isCorrect 状态就想同时驱动「提示消息」和「答案列表样式」——会导致两个视觉变化完全同步、没办法错开时间点,动画效果显得生硬
  • 答错后不重置 isCorrectundefined——连续两次答错时,状态值都是 false、没有变化,React 不会重新触发渲染,第二次点错看起来像没反应
  • 多行 JSX 忘记用括号包起来——JS 的自动分号插入会在换行处提前结束 return 语句,导致后面的 JSX 内容被当成死代码,函数实际上什么都没返回

延伸 / 后续讲座会用到

前台交互到这里基本完成,下一讲要回到编辑器后台,让站长可以为每个 Block 实例设置背景色和文字对齐方式(左/中/右)。


Sources

Udemy:

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