React useEffect 实践:React 实现翻牌游戏项目
下面我们通过讲解一下使用 React 实现 翻牌游戏的小项目,进一步讲解 React Effect 的一些知识。
最终效果如下:
这个游戏的组件设计也很简单,只有 Card、Board 和用来装 Score 这些东西的 Header。
下面我们直接讨论游戏的流程引起的状态变化。首先先设计如下的状态:
const [cards, setCards] = useState([]);
const [flipped, setFlipped] = useState([]);
const [locked, setLocked] = useState(false);
const [score, setScore] = useState(0);
const [best, setBest] = useState(() => {
const v = localStorage.getItem("memory_best");
return v ? Number(v) : 0;
});
const [loading, setLoading] = useState(true);
const [errors, setErrors] = useState(null);
在开始之前,我们先复习一下 useEffect 的使用场景:
- 与“外界”交互。
- 响应
props或state的变化来执行一段逻辑。
首先是游戏刚开始的时候:我们需要从外部 API fetch 需要的数据,所以我们需要 useEffect。这个操作只有在刚进入的时候需要调用,因此依赖数组为 []。这个流程包含了如下步骤:
- 切换 Loading 模式:
setLoading(true)。设置一个mount标记位(这是一个常用技巧)。 - 开始加载卡片初始数据,然后把加载到的卡片放到 cards 里面,设置好其他的初始值。如果报错了就
setErrors。 - 不管成功与否,最后关掉 Loading 模式。
// fetch images -> build pairs on mount or when pairCount changes
useEffect(() => {
let mounted = true;
setLoading(true);
setErrors(null);
async function init() {
try {
const images = await fetchImages(pairCount);
if (!mounted) return;
const pairs = makePairs(images);
setCards(shuffle(pairs));
setScore(0);
setFlipped([]);
} catch (err) {
if (!mounted) return;
setErrors(err?.message || "Failed to fetch images");
} finally {
if (mounted) setLoading(false);
}
}
init();
return () => {
mounted = false;
};
}, [pairCount]);
然后是翻牌的逻辑:如果翻到的两张牌匹配了,就标记上 isMatched 并加分,否则直接 return。一旦有两张牌被翻,这个逻辑就被触发:
useEffect(() => {
if (flipped.length !== 2) return;
const [id1, id2] = flipped;
const c1 = cards.find((c) => c.id === id1);
const c2 = cards.find((c) => c.id === id2);
if (!c1 || !c2) return;
if (c1.pairId === c2.pairId) {
setCards((prev) =>
prev.map((c) => (c.pairId === c1.pairId ? { ...c, isMatched: true } : c)),
);
setFlipped([]);
setScore((s) => s + 1);
return;
}
setLocked(true);
const t = setTimeout(() => {
setFlipped([]);
setLocked(false);
}, 700);
return () => {
clearTimeout(t);
};
}, [flipped, cards]);
// persist best score
useEffect(() => {
if (score > best) {
setBest(score);
localStorage.setItem("memory_best", String(score));
}
}, [score, best]);
然后剩下的逻辑就都是一些对事件响应的回调了,这里略过:
// clickCard: keep the callback stable. We avoid depending on `cards` here
const clickCard = useCallback(
(id) => {
if (locked) return;
setFlipped((prev) => {
// prevent duplicate or third flips
if (prev.includes(id) || prev.length >= 2) return prev;
if (prev.length === 0) return [id];
return [...prev, id];
});
},
[locked],
);
const reset = useCallback(() => {
setScore(0);
setFlipped([]);
setLocked(false);
setCards((prev) => shuffle(prev.map((c) => ({ ...c, isMatched: false }))));
}, []);
const newGame = useCallback(async () => {
setLoading(true);
setErrors(null);
try {
const images = await fetchImages(pairCount);
setCards(shuffle(makePairs(images)));
setScore(0);
setFlipped([]);
setLocked(false);
} catch (err) {
setErrors(err?.message || "Failed to fetch images");
} finally {
setLoading(false);
}
}, [pairCount]);
这里,我们把所有事件触发的回调放在了 hooks 中。如果回调比较简单的话,也可以直接在对应 component 里面实现了。