שלד לפיתוח יישום Chat עם React ו Firebase
פוסט זה כולל טיפ קצר לעבודה עם React. אם אתם רוצים ללמוד איתי ריאקט מההתחלה ובצורה מקצועית תשמחו לשמוע שבניתי קורס מלא הכולל עשרות שיעורי וידאו והמון תרגול בו לומדים ריאקט מההתחלה ועד לנושאים המתקדמים.
לפרטים נוספים והרשמה בקרו בדף קורס ריאקט כאן באתר.
אחד השילובים שאני יותר אוהב בפיתוח הוא Redux ו Web Sockets. זה עובד ממש פשוט ויפה יחד: כל פעם שמגיע אירוע מבחוץ דרך ה Web Socket, יהיה לנו מידלוור שיתרגם את האירוע לאוביקט Redux Action, האוביקט ייכנס למערכת דרך ה Reducers ובאופן אוטומטי המסך יתעדכן כי ריאקט יזהה את השינוי וימשוך את המידע החדש.
בואו נכתוב שלד קצר להמחיש את הרעיון באמצעות שרת Firebase.
1. מבנה ה State וה Reducer
האפליקציה שאני בונה מאוד פשוטה. היא כוללת תיבה לכתיבת הודעה, רכיב שמראה את רשימת כל ההודעות מהשרת וקצת קוד לחבר בין הממשק ל Firebase. אוביקט המידע לכן כולל בסך הכל שני שדות מעניינים:
const state = {
account: {
username: 'ynon',
},
messages: {
messages: [],
},
};
החלוקה לשני אזורים תאפשר לי להגדיר שני Reducers שונים: אחד שיטפל בכל מה שקשור בחשבון והשני יטפל בכל מה שקשור בהודעות. בעבודה עם פיירבייס אני "אקשיב" לטבלת ההודעות וכל פעם שיש הודעה חדשה אקבל עדכון. בשביל הדוגמא אפילו לא חייבים לקחת רק את ההודעות החדשות, ויהיה יותר קל בכל עדכון פשוט למחוק את כל רשימת ההודעות הקודמת ולהחליף אותה בחדשה.
בשביל זה כתבתי את ה Reducer הבא בקובץ messages.js:
import produce from 'immer';
const initialState = {
messages: [],
};
export default produce((state, action) => {
switch(action.type) {
case 'RESET_MESSAGES':
state.messages = action.payload;
break;
}
}, initialState);
לידו אני מחזיק Reducer נוסף שמטפל בשינוי שם המשתמש. קראתי לו account.js וזה הקוד שלו:
import produce from 'immer';
const initialState = {
username: "guest",
};
export default produce((state, action) => {
switch(action.type) {
case 'SET_USERNAME':
state.username = action.payload;
break;
}
}, initialState);
2. ממשק המשתמש
הרכיב הראשון בממשק המשתמש הוא הפקד Username שאחראי על הצגה ושינוי שם המשתמש. הפקד מחובר ל Redux ושולף מה Store את שם המשתמש, ובתורו יפעיל את dispatch כדי לשנות שם משתמש אם הטקסט בתיבה מתעדכן:
import React from 'react';
import { connect } from 'react-redux';
import { setUsername } from './redux/actions';
function mapStateToProps(state) {
return {
username: state.account.username,
};
}
export default connect(mapStateToProps)(function Username(props) {
const { username, dispatch } = props;
function handleChange(e) {
dispatch(setUsername(e.target.value));
}
return (
<div className='username'>
<label>
User Name:
<input type="text" value={username} onChange={handleChange} />
</label>
</div>
);
});
פקד יותר מעניין הוא הפקד Messages. פקד זה מראה את רשימת ההודעות ומאפשר לכתוב הודעה חדשה. בגלל שבכל הודעה יש לציין גם את שם המשתמש ששלח אותה הפקד יהיה תלוי גם בשדה username מאוביקט ה State (אפילו ששם המשתמש לא מוצג בפקד בצורה ויזואלית). הקוד נראה כך:
import React from 'react';
import { connect } from 'react-redux';
import { writeToFirebase } from './redux/actions';
import { useState } from 'react';
function mapStateToProps(state) {
return {
messages: state.messages.messages,
username: state.account.username,
}
}
export default connect(mapStateToProps)(function Messages(props) {
const { messages, username } = props;
const [message, setMessage] = useState('');
function sendMessage() {
writeToFirebase(username, message);
setMessage('');
}
return (
<>
<input type="text " value={message} onChange={(e) => setMessage(e.target.value) }/>
<button onClick={sendMessage}>Send</button>
<ul>
{messages.map(msg => (
<li key={msg.id}>
<b>From: {msg.from}</b> {msg.text}
</li>
))}
</ul>
</>
);
});
3. ה Action שכותב ל Firebase
הכתיבה ל Firebase לוקחת בסך הכל שלוש שורות: אנחנו מתחברים לטבלא, מייצרים timestamp ושולחים את ההודעה, שם השולח וה timestamp לשרת:
export function writeToFirebase(from, text) {
const msgs = firebase.firestore().collection('messages');
const timestamp = new Date();
msgs.add({ from, text, timestamp });
}
הייתי שמח אם פיירבייס היה שומר זמנים בשבילי כי בצורה כזו לקוחות יכולים לזייף את שעת השליחה, אבל בשביל הדוגמא אני מוכן לחיות עם זה. במערכת גדולה יותר ייתכן והיינו מעדיפים להשתמש בקוד צד שרת שלנו שיתווך את הכתיבה לפיירבייס.
4. ה Middleware שקורא מ Firebase
והחלק הכי מעניין בשלד הזה הוא ה Redux Middleware. בארכיטקטורת רידאקס המידלוור הוא זה שמאזין לאירועים שנכנסים ומתרגם אותם להודעות שה Reducers שלנו יודעים לעכל. במקרה של פיירבייס ההודעה שתגיע מהם כוללת המון מידע: היא כוללת את כל ההודעות בטבלא מסודרות לפי הסדר שבחרנו (או אם החלטנו לחלק לעמודים, אז את העמוד הנוכחי של נתונים), ואת רשימת השינויים מאז הפעם הקודמת שקיבלנו הודעה כדי שנוכל לעדכן את הממשק רק עם מה שהשתנה. אני אתן ל Redux ולריאקט לזהות את השינויים ולכן אשמח לקחת את כל ההודעות ולייצר מהן אוביקט עדכון אחד.
מידלוור שמאזין לקוד אסינכרוני ייכתב כמעט תמיד לפי התבנית:
המתנה לאירוע START ואחריו המידלוור יירשם לשירות האסינכרוני.
המתנה להודעות מהשירות האסינכרוני, ותרגום כל הודעה לאוביקט Action.
שליחת כל Action דרך dispatch ל Reducers לטיפול והכנסה למערכת.
במקרה של פיירבייס הקוד נראה כך:
const firebaseMessages = ({dispatch, getState }) => next => action => {
if (action.type === 'FIREBASE_INIT') {
firebase.firestore().collection('messages').orderBy('timestamp').onSnapshot(function(qs) {
const batch = [];
qs.forEach(function(doc) {
batch.push({ id: doc.id, ...doc.data()});
})
dispatch({ type: 'RESET_MESSAGES', payload: batch });
});
return;
}
return next(action);
};
הקוד מתעורר כשיש הודעה או הודעות חדשות, מייצר אוביקט מסוג RESET_MESSAGES
שמכיל את כל ההודעות ומפעיל dispatch על האוביקט שבנה.
את האיתחול נרצה לעשות אחרי שניצור את ה Store ולכן המשך הקוד נראה כך:
const store = createStore(reducer, applyMiddleware(firebaseMessages));
export default store;
store.dispatch({ type: 'FIREBASE_INIT' });
אנחנו יוצרים את ה Store ומיד אחר כך שולחים אירוע FIREBASE_INIT
ל Middleware.
הייתי מעלה את הדוגמא ל codesandbox שתשחקו איתה אבל פעם קודמת שעשיתי דבר דומה מהר מאוד הפיירבייס התמלא בהמון הודעות מוזרות שהוצגו על המסך פה בבלוג אז אני משאיר כאן רק את קטעי הקוד החשובים בלי החלק שמחבר את הקוד לשרת ובלי מזהי השרת שלי.