ItIron2023
react
我們昨天做了一個還算有趣的問題,利用state控制整個emoji陣列並決定每個組件要在螢幕的哪處渲染,今天我們則要延續昨天的題目提出一些後續的需求,這也是當時面試時我被要求做的事情,一起來看一下我當時的情境吧!
請觀察這個codesandbox,基本上就是昨天的完成版,有確實達到昨天的題目要求,今天你需要加入一些程式碼滿足以下的需求。
1. 當emoji渲染後2秒讓該emoji出現反覆簡單的心跳動畫效果(變大再變小)
2. 點擊畫面上的按鈕時須toggle所有emoji的動畫狀態(一起開始/停止)
3. 點擊按鈕時,按鈕上不能出現emoji,請阻止emoji在按鈕上出現
最終成品應如下圖所示,你可以自行決定動畫的長度與縮放的大小。
請由這份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倍後瞬間縮水回原本大小。
剩下的部分就是在對應的情況加入這個.heartbeat
class了,我們知道要在組件mount的兩秒後加入動畫效果,那麼這聽起來像是一個useEffect的工作,同時也需要另一個state來決定這個組件現在是否要加入.heartbeat
class,請修改你的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情境來解決,到現在我仍覺得這是個有趣的問題,也算同時檢測很多項能力,缺點則是情境本身不夠實務,不過作為面試題我想綽綽有餘了!我們接著就剩下最後三個問題,都不會太過於困難,一起走到最後吧!
本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!