iT邦幫忙

2023 iThome 鐵人賽

DAY 27
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 27

30天React練功坊-攻克常見實務/面試問題 Day27: Add emojis to the page onclick PartII(interview question)

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們昨天做了一個還算有趣的問題,利用state控制整個emoji陣列並決定每個組件要在螢幕的哪處渲染,今天我們則要延續昨天的題目提出一些後續的需求,這也是當時面試時我被要求做的事情,一起來看一下我當時的情境吧!

本日題目

請觀察這個codesandbox,基本上就是昨天的完成版,有確實達到昨天的題目要求,今天你需要加入一些程式碼滿足以下的需求。

1. 當emoji渲染後2秒讓該emoji出現反覆簡單的心跳動畫效果(變大再變小)
2. 點擊畫面上的按鈕時須toggle所有emoji的動畫狀態(一起開始/停止)
3. 點擊按鈕時,按鈕上不能出現emoji,請阻止emoji在按鈕上出現

最終成品應如下圖所示,你可以自行決定動畫的長度與縮放的大小。

day27-demo-gif

請由這份starter code開始

import React, { useState, useEffect } from "react";

const Emoji = ({ x, y, emoji }) => {
  return (
    <span
      className="emoji"
      role="img"
      style={{
        position: "absolute",
        left: `${x - 24}px`,
        top: `${y - 24}px`
      }}
    >
      {emoji}
    </span>
  );
};

function App() {
  const [emojis, setEmojis] = useState([]);
  const fruitEmojis = ["🍎", "🍊", "🍇", "🍓", "🍒"];

  const handleClick = (e) => {
    const x = e.clientX;
    const y = e.clientY;
    const randomEmoji =
      fruitEmojis[Math.floor(Math.random() * fruitEmojis.length)];
    // 每次點擊時更新emoji陣列
    setEmojis((prevEmojis) => [
      ...prevEmojis,
      { id: Date.now(), x, y, emoji: randomEmoji }
    ]);
  };

  return (
    <div
      style={{ width: "100vw", height: "100vh", position: "relative" }}
      onClick={handleClick} // 加入click handler
    >
      <h1>Click to add an fruit emoji 🍎🍊🍇</h1>
      {emojis.map((emojiObj) => (
        <Emoji
          key={emojiObj.id}
          x={emojiObj.x}
          y={emojiObj.y}
          emoji={emojiObj.emoji}
        />
      ))}
    </div>
  );
}

export default App;

解答與基本解釋

starter code有提供基礎結構,這個問題其實稱不上太複雜,只要注意幾個我們之前講過的東西就不至於太過於無助,只有第三個要求會稍微特別一些,我們先處理第一個需求!

當emoji渲染後2秒讓該emoji出現反覆簡單的心跳動畫效果(變大再變小)

首先你會先需要處理css檔案,增加一個處理動畫的keyframe與對應的class,手刻動畫的需求雖然會很看產業,但多少你都需要有所接觸,請在styles.css加入簡單的動畫程式碼,這部分通常需要做一些簡單的查詢,不過這類的情境面試通常都是允許查詢或請面試官協助的。

.heartbeat {
  animation: heartbeat 0.5s ease-in-out infinite alternate;
}

@keyframes heartbeat {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(1.5);
  }
}

特別注意我這邊用了alternate讓動畫保留完成的效果,會更好的模擬心跳的情況,否則他會scale到1.5倍後瞬間縮水回原本大小。

剩下的部分就是在對應的情況加入這個.heartbeatclass了,我們知道要在組件mount的兩秒後加入動畫效果,那麼這聽起來像是一個useEffect的工作,同時也需要另一個state來決定這個組件現在是否要加入.heartbeatclass,請修改你的Emoji組件程式碼。

const Emoji = ({ x, y, emoji, paused }) => {
  // 加入state管理動畫是否呈現
  const [animated, setAnimated] = useState(false);
  
  // 利用useEffect在兩秒後變更state
  useEffect(() => {
    const timeoutId = setTimeout(() => setAnimated(true), 2000);
    return () => clearTimeout(timeoutId);
  }, []);

  return (
    <span
      // 動態變更className  
      className={`emoji ${animated && "heartbeat"}`}
      role="img"
      style={{
        position: "absolute",
        left: `${x - 24}px`,
        top: `${y - 24}px`
      }}
    >
      {emoji}
    </span>
  );
};

特別注意在useEffect使用timer需要加入對應的cleanup function,這也是我們之前提過的議題,避免出現memory leak的情況。

這麼一來第一個問題就解決了,這也是最難的部分,接著馬上來處理第二個需求吧!

點擊畫面上的按鈕時須toggle所有emoji的動畫狀態(一起開始/停止)

關鍵字在一起開始/停止這一點,這會關係到你該在哪一層加入這個state管理,既然所有的Emoji組件都要一起被控制,那麼你最好的選擇便是在App組件加入對應的state管理,同時將這個state作為props傳給Emoji組件,讓它知道現在動畫該開始還是停止。

接著則是加入一個按鈕去toggle這個state,請先修改你的app組件,加入以下的部分。

function App() {
  const [paused, setPaused] = useState(false); // 增加paused state

  const handlePausedToggle = (e) => { // 新增toggle function
    setPaused(!paused);
  };

  return (
    <div>
      <button onClick={handlePausedToggle}>Click me to toggle animation</button>
      {emojis.map((emojiObj) => (
        <Emoji
          key={emojiObj.id}
          x={emojiObj.x}
          y={emojiObj.y}
          emoji={emojiObj.emoji}
          paused={paused} // 傳入新的props
        />
      ))}
    </div>
  );
}

export default App;

最後只要在Emoji組件接受這個props並更新剛剛關於動畫的控制就行了

const Emoji = ({ x, y, emoji, paused }) => {
  // 上略
  return (
    <span
      className={`emoji ${animated && !paused && "heartbeat"}`} // 修改這一行
      //下略
    >
      {emoji}
    </span>
  );
};

點擊按鈕時,按鈕上不能出現emoji,請阻止emoji在按鈕上出現

最後一個問題我想了解DOM操作的人恐怕早就想到了,當我今天點擊按鈕後,最終事件會冒泡上去然後觸發貼上emoji的事件.那麼你要做的就是阻止事件繼續往上冒泡即可,請修改我們原本的handlePausedToggle函數

const handleButtonClick = (e) => { 
  e.stopPropagation(); // 加入這行
  setPaused(!paused);
};

總結

今天我們延伸昨天的問題多做了幾個需求,需要仰賴基本的css動畫以及我們之前討論過的useEffect timer情境來解決,到現在我仍覺得這是個有趣的問題,也算同時檢測很多項能力,缺點則是情境本身不夠實務,不過作為面試題我想綽綽有餘了!我們接著就剩下最後三個問題,都不會太過於困難,一起走到最後吧!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day26: Add emojis to the page onclick(interview question)
下一篇
30天React練功坊-攻克常見實務/面試問題 Day28: Fetch all synonyms for given word(interview question)
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言