מדריך קוד: בואו נכתוב משחק זיכרון ב Redux ו React
פוסט זה כולל טיפ קצר לעבודה עם React. אם אתם רוצים ללמוד איתי ריאקט מההתחלה ובצורה מקצועית תשמחו לשמוע שבניתי קורס מלא הכולל עשרות שיעורי וידאו והמון תרגול בו לומדים ריאקט מההתחלה ועד לנושאים המתקדמים.
לפרטים נוספים והרשמה בקרו בדף קורס ריאקט כאן באתר.
לפני שנתיים כתבתי כאן משחק זיכרון עם ריאקט בעזרת הפונקציה useReducer. היום אני רוצה לנסות משחק דומה עם Redux כדי לראות מה התחדש ברידאקס בשנים האחרונות ובמיוחד כדי לשחק עם Redux Toolkit, ספריה שהופכת את הכתיבה ברידאקס למשחק הרבה יותר מהנה.
1. הסוד של רידאקס טולקיט הוא בכלל Immer
האמת חייבת להיאמר, כתיבת קוד JavaScript שמתעסק עם Immutable Data זה לא תענוג. אם אני צריך לקחת אוביקט סטייט שמורכב מאוביקטים פנימיים ולשנות משהו באחד האוביקטים הפנימיים שלו אני מהר מאוד מוצא את עצמי כותב דברים כאלה:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
}
}
ומפה הדרך לאבדון קצרה.
הטריק הכי מלהיב עם Redux Toolkit זה החיבור המובנה שלה עם Immer. אימר, למי שלא מכיר, זו ספריית JavaScript שמאפשרת לי לכתוב קוד שמשנה מידע והופכת אותו אוטומטית לקוד שמשכפל את המידע במקום לשנות אותו.
הפונקציה הכי חשובה של Redux Toolkit נקראת createSlice
והיא בעצם יוצרת גם Reducer וגם Action Creators, משולבת עם Immer כך שהקוד שאני כותב בה "כאילו" משנה את האוביקטים.
בואו נצלול למשחק הזיכרון כדי לראות את הסיפור הזה בפעולה. כל הקוד שאני כותב עליו זמין בריפו הזה: https://github.com/ynonp/redux-memory-game
אז במשחק זיכרון יש לי חבילה של קלפים שאני יכול ליצור עם:
const initialState = {
cards: _.shuffle(_.range(10).map(i => ({
value: Math.floor(i / 2),
hidden: true,
found: false,
}))),
};
המאפיין value של קלף מייצג את המספר שעליו, hidden אומר אם הוא הפוך ו found אומר אם מצאנו אותו יחד עם הזוג שלו.
הפונקציה הראשונה שאני צריך במשחק זיכרון היא פונקציה שנקראת כשמשתמש לוחץ על קלף. הנה המימוש המפתיע שלי:
function onPlay(state, action) {
const index = action.payload;
if (state.cards[index].hidden) {
state.cards[index].hidden = false;
}
const visibleCards = state.cards.filter(c => !c.found && !c.hidden);
if (visibleCards.length === 2) {
if (visibleCards[0].value === visibleCards[1].value) {
visibleCards[0].found = true;
visibleCards[1].found = true;
}
}
}
ולמה זה מפתיע? כי אנחנו יודעים שב Redux כל Reducer צריך לקבל state ו action ולהחזיר state חדש. פה אני פשוט משנה את ה state במה שנראה כמו שינוי במקום:
state.cards[index].hidden = false;
באופן אוטומטי ספריית immer הופכת את הקוד הזה לקוד שלא באמת משנה את הסטייט אלא יוצר סטייט חדש ומחזיר אותו.
אגב פונקציה שניה שתעזור לי במשחק זיכרון תופעל כמה שניות אחרי התור השני בשביל להפוך את כל הקלפים שצריך להסתיר:
function onCheck(state) {
for (let c of state.cards) {
if (!c.found) {
c.hidden = true;
}
}
}
את כל הקוד הזה כתבתי בקובץ שנקרא memorySlice.js
ועד עכשיו לא היה שום דבר שקשור לרידאקס או ל redux-toolkit בקובץ. החלק האחרון הוא כבר נקודת הכניסה ל Redux:
export const memorySlice = createSlice({
name: 'memory',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: { play: onPlay, check: onCheck },
});
הפונקציה createSlice
מקבלת את שתי הפונקציות שכתבתי לתוך מפתח reducer של אובייקט הפרמטרים שלה ויוצרת גם Reducer מסודר וגם Action Creators. עכשיו אני יכול להשתמש בערך ההחזר שלה כדי לייצא את התוצאות:
export const { play, check } = memorySlice.actions;
export default memorySlice.reducer;
לסיום הקובץ אני יוצר ומייצא עוד שתי פונקציות עזר איתן קוד מקבצים אחרים יוכל לקרוא מידע מה State:
export const selectVisibleCards = (state) => state.memory.cards.filter(c => !c.hidden && !c.found);
export const selectCards = (state) => state.memory.cards;
2. איך משתמשים ב Reducers
בתיקיית src/app
של הפרויקט תוכלו למצוא קובץ בשם store.js
שמחזיק את התוכן הבא:
import { configureStore } from '@reduxjs/toolkit';
import memoryReducer from '../features/memory/memorySlice';
import memory from '../features/memory/memoryMiddleware';
export const store = configureStore({
reducer: {
memory: memoryReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(memory),
});
תכף נדבר על המידלוור, אבל בינתיים בואו נתמקד ב Reducer - הפונקציה configureStore
, גם היא מ Redux Toolkit, מחזירה Redux Store לפי רשימת ה Reducers שתעבירו לה, ואת ה Reducer אני מייבא מהקובץ הקודם שכתבנו.
3. קומפוננטת ריאקט
החלק הכי טוב בעבודה עם רידאקס הוא שכל הלוגיקה כבר קיימת בלי קשר לממשק, ולכן כשאני מגיע לכתוב קומפוננטת ריאקט אני בסך הכל צריך להבין איזה "מידע" משפיע על הקומפוננטה ואיזה פעולות היא יכולה לעשות.
במקרה של משחק הזיכרון המידע הוא רשימת הקלפים והפעולה היחידה היא ללחוץ על קלף. הנה הקוד של הקומפוננטה:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
play,
selectCards,
} from './memorySlice';
import './memory.css';
export function MemoryGame() {
const cards = useSelector(selectCards);
const dispatch = useDispatch();
return (
<div>
{cards.map((c, i) => (
<div
className={c.hidden
? "card hidden" : c.found
? "card found" : "card"
}
onClick={() => dispatch(play(i))}
key={i}
>
{c.value}
</div>
))}
</div>
);
}
4. המידלוור
עד לפה היה יכול להיות לנו קוד שמראה רשימת קלפים וחושף קלפים כשאנחנו לוחצים עליהם, וגם צובע בצהוב קלפים זהים שמצאנו. בשביל משחק אמיתי אנחנו צריכים גם להסתיר בחזרה את הקלפים וכאן Redux Middleware הוא פיתרון שמתאים כמו כפפה ליד:
כל פעם שאני שולח פקודת play המידלוור יכול להריץ את הפקודה ולבדוק אם אחרי שהסתיימה יש שני קלפים גלויים שעדיין לא מצאנו את הזוג שלהם. אם כן, הוא ישים טיימר של שתי שניות ובעוד שתי שניות ישלח בקשה להפוך אותם.
כל פעם שאני מקבל פקודת play לפני הרצת הפקודה המידלוור יבדוק אם יש איזה טיימר פעיל, ואם כן הוא יבטל אותו ופשוט יריץ מיד את האיפוס. המחשבה כאן היא שאם מישהו לוחץ על קלף "שלישי" צריך מיד להסתיר את שני הגלויים מהתור הקודם.
הקוד שמתאים לתיאור המילולי שלי הוא בסך הכל:
const gameMiddleware = ({ dispatch, getState }) => next => action => {
// before play, if we're waiting for a "check" we'll
// cancel the timer, check immediately and then continue
// to play
if (action.type === play.type && activeTimer) {
clearTimeout(activeTimer);
activeTimer = null;
dispatch(check());
}
let result = next(action)
// after play we count 2 seconds and check the status
if (action.type === play.type) {
if (selectVisibleCards(getState()).length === 2) {
activeTimer = setTimeout(() => {
dispatch(check());
}, 2000);
}
}
return result
};
וזה בעצם כל משחק הזיכרון שלנו. הנה מה שהרווחנו בעבודה עם Redux Toolkit:
כל הלוגיקה מרוכזת בפונקציות פשוטות, שמקבלות אוביקט סטייט ואוביקט action ומשנות את הסטייט לפי הבקשה ב action.
אם בעתיד נרצה להוסיף מנגנונים, מאוד ברור לנו איך לעשות את זה בלי "לקלקל" או לשנות את הקוד הקיים. פשוט צריך להוסיף עוד סלייס. הקוד היחיד שאצטרך לשנות הוא הקובץ store.js, כדי להוסיף שם התיחסות לסלייס החדש.
קומפוננטת ריאקט שיצרתי יצאה מאוד פשוטה. הלוגיקה היחידה שבה היא לוגיקה שקשורה לתצוגה.
למרות שלא הראיתי כאן, יהיה קל מאוד לכתוב קוד בדיקה ללוגיקה כי כולה מורכבת מפונקציות טהורות.
למרות שלא הראיתי כאן, רידאקס טולקיט מגיע עם כל כלי ה debug של רידאקס אז יש לכם את כל הפינוקים של מסע אחורה בזמן וצפיה בכל ה actions שעוברות במערכת.
סך הכל כתיבת רידאקס היום היא הרבה יותר פשוטה ממה שהיתה בעבר והרבה בזכות redux toolkit.
את קוד המשחק המלא אתם יכולים למצוא במאגר כאן: https://github.com/ynonp/redux-memory-game