ToCode
Ir al canal en Telegram
1 419
Suscriptores
Sin datos24 horas
Sin datos7 días
-530 días
Archivo de publicaciones
1 419
תודה שאמרת לי
בגלל ש AI הוא לא בן אדם, כשהוא כותב קוד הוא הרבה פעמים מתעלם מדברים שהוא כבר יודע. לדוגמה בן אדם שכותב קוד נניח בפלאסק ומכיר את הפונקציה
url_for יעדיף תמיד להשתמש בה בלינקים ויכתוב תגית a עם:
<a href="{{ url_for('tasks') }}">All Tasks</a>
אבל AI שכותב קוד, למרות שהוא מכיר את url_for יותר טוב ממך, עדיין יכול לכתוב:
<a href="/tasks">All Tasks</a>
ואז כשאתה שואל אותו משהו כמו "למה לא השתמשת ב url_for" הוא מיד מתנצל עם איזה "תודה שאמרת לי וכל הכבוד ששמת לב, והנה אני מתקן את הקוד".
כשזה קורה התגובה הראשונה שלי היא להתעצבן על המחשב - מה זה אומרת תודה שאמרת לי!? אם ידעת ש url_for עדיף למה לא כתבת את זה מההתחלה???
אבל השנים לימדו אותי שבעיות לא נפתרות כשצועקים על המחשב. במקום אני משתדל לזכור את הפרומפט הבא אחרי שקיבלתי מימוש מ AI:
> Please perform a full code review
רוב הזמן באיטרציה השניה נקבל קוד ברמה יותר גבוהה.1 419
ההערה הזאת היא בכלל פיצ'ר
נניח שבניתם פיצ'ר של חיפוש לאתר. הפיצ'ר על עוד בפרודקשן והתעסקתם עם המון דברים במהלך הפיתוח ואתם במתח לראות מי יחפש מה ואם המנגנון יצליח למצוא תוצאות רלוונטיות, ואז אתם מקבלים ב Code Review את ההערה:
> Consider adding rate limiting to the search endpoint
יודעים מה? בואו נסבך עוד קצת את הסיטואציה, ונניח שמי שנתן את ההערה הוא בכלל AI עכשיו לשאלה, מה עושים עם זה?
מצד אחד ברור ש Rate Limit זה רעיון טוב, מי יודע איזה בעיות יהיו אם בוטים מרושעים יפציצו את ה Endpoint בבקשות חיפוש, וזה גם נכון שנקודת קצה של חיפוש יכולה להיות יותר רגישה למתקפות DoS כי חיפוש דורש יותר משאבי מערכת. מצד שני אנחנו כבר עובדים על החיפוש הזה תקופה ארוכה, עד עכשיו התמקדמו בלהחזיר את התוצאות הטובות ביותר ואנחנו עדיין לא בטוחים שאנחנו בכלל בכיוון לגבי זה. למי יש זמן להוסיף עכשיו מנגנון Rate Limit שאולי יקלקל דברים או ידחה את העלייה לפרודקשן רק בגלל סיכון אבטחה פוטנציאלי עתידני. מצד שלישי למערכות שונות יש רגישות שונה לתקלות, אולי אתם חיים בעולם שלא יסלח לכם על נפילה בעקבות מתקפת DoS.
אני חושב שיותר נכון להסתכל על הערה מהסוג הזה בתור Feature Request יותר מאשר הערת מימוש. במקרה של Rate Limit זה משהו שצריך לעלות בעקבות Security Review על גירסה. לפעמים ה Security Review קורה על כל גירסה לפני העלאה ואז נקבל דוח ונתרגם אותו למשימות שיהפכו לקומיטים, לפעמים ה Security Review יבוא אחרי מתקפת ה DoS הראשונה.
הפרדת ההרחבות למנגנון אחר (אולי לפתוח כרטיס חדש בג'ירה, אולי לתקן את תהליך העבודה ולהוסיף שלבים) יכולה לעזור לנו להתמקד בפיתוח ולהביא מהר יותר קוד טוב יותר לפרודקשן.
1 419
חידת Vue: ריאקטיביות
נתון קוד Vue הבא:
<script setup lang="ts">
import {ref, computed} from 'vue';
const data = { count: 0 };
const value = ref(data);
function btn1() {
data.count = 5;
}
function btn2() {
value.value.count++;
}
</script>
<template>
<div>
<p>
Value is: <span>{{ value.count }}</span></p>
<button @click="btn1">Button 1</button>
<button @click="btn2">Button 2</button>
<hr />
</div>
</template>
מה יופיע על המסך אחרי לחיצה על שני הכפתורים לפי הסדר? מה יקרה אם נלחץ רק על הראשון? למה זה קורה?
מה יקרה אם נשנה את התבנית ל:
<template>
<div>
<p>
Value is: <span>{{ data.count }}</span></p>
<button @click="btn1">Button 1</button>
<button @click="btn2">Button 2</button>
<hr />
</div>
</template>
מה יהיו ערכי המשתנים אחרי לחיצה על הכפתורים? מה יופיע על המסך?1 419
טיפ טייפסקריפט: יבוא קבצי JSON גדולים
לטייפסקריפט יש פיצ'ר ממש חמוד לעבודה עם קבצי JSON שנקרא resolveJsonModule. אנחנו מגדירים ב tsconfig.json את האופציה באופן הבא:
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": ["src"]
}
ואז כותבים:
import settings from './settings.json';
ובאופן אוטומטי טייפסקריפט קורא את הקובץ settings.json, מזהה לבד את טיפוס הנתונים בקובץ ומגדיר את האוביקט settings בצורה נכונה, כלומר אם הקובץ שלי מכיל את התוכן:
[{
"repo": "TypeScript",
"dry": false,
"debug": false
}]
ואני אנסה לכתוב בקוד:
import settings from './settings.json';
console.log(settings[0].foo);
טייפסקריפט יצעק שאין מאפיין foo לאוביקטים במערך. קסם נכון? כמעט. הבעיה מתחילה כשמנסים לטעון אוביקט JSON גדול, למשל 10 מגה. קובץ כזה מאט את כל ה VS Code ובסוף טייפסקריפט לא מצליח לקרוא אותו כי הוא גדול מדי ואנחנו תקועים בלי הגדרת טיפוסים.
בעבודה עם קבצי JSON גדולים שווה להכיר את האופציה allowArbitraryExtensions של טייפסקריפט, אופציה שבעזרתה נוכל להגדיר את הטיפוס של ה JSON ידנית, נחסוך לטייפסקריפט עבודה וכך נקבל VS Code מהיר יותר והגדרות טיפוסים אמינות. ה tsconfig.json נראה ככה:
{
"compilerOptions": {
"allowArbitraryExtensions": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": ["src"]
}
ובתיקיית src אנחנו יוצרים קובץ בשם של ה json אבל עם d באמצע וסיומת ts, לדוגמה אם ל json שלי קוראים deposits.json אז אני יוצר קובץ בשם deposits.d.json.ts. בקובץ הגדרת הטיפוס אני כותב:
export interface SavingsData {
INTERESTSDATE: string;
SAVINGSPROGRAMBYAGE: string;
AGE: string;
BANK: string;
SAVINGSPLAN: string;
SAVINGSPERIOD: string;
FIXEDWITHOUTLINKAGEVAL: number;
FIXEDLINKEDCONSUMERPRICEINDVAL: number;
VARIABLESPREAD: number;
}
declare const array: Array<SavingsData>;
export default array;
ומפה אנחנו מסודרים. אפשר לייבא את הקובץ deposits.json אפילו שהוא שוקל 10 מגה וטייפסקריפט ישתמש בהגדרת הטיפוס שבנינו במקום לקרוא את הקובץ ולנסות להבין את הטיפוס לבד.1 419
למה קשה לנו עם "בעצם זה לא היה רעיון טוב"
אחד האתגרים המשמעותיים בבניית צוות הוא בחירת המינון והתיזמון של "בעצם זה לא היה רעיון טוב". מתכנתים שאומרים את זה יותר מדי פעמים או מוקדם מדי לא מצליחים להתקדם. אלה שיגידו את זה פחות מדי פעמים או מאוחר מדי ייתקעו בכיוון הלא נכון ובסוף לא יגיעו ליעד.
מותר לעבור קופה בסופר כשגילינו שהתור ארוך מדי אבל לא כדאי לעבור יותר מפעם אחת בקנייה.
בעבודה על פרויקט תוכנה כדאי תמיד לחשוב על היעד ולא רק על העלות הנוכחית. לדוגמה התחלתי לבנות פרויקט ואחרי שלושה חודשים של פיתוח העליתי גירסה ראשונה לפרודקשן וגיליתי שבחרתי בסיס נתונים לא מתאים. על פניו שינוי בסיס נתונים עכשיו יכול להחזיר אותי שלושה חודשים אחורה ולכן שווה לעשות מאמץ ולגרום לבסיס הנתונים שבחרתי לעבוד. אבל למעשה מערכת תוכנה יכולה להיות בשימוש שנים ארוכות, והישארות עם בסיס נתונים לא מתאים זה נזק שאני אצטרך לחיות איתו עכשיו שנים קדימה.
"בעצם זה לא היה רעיון טוב" צריך להיות חלק מהלקסיקון שלנו בפיתוח פרויקטים. לא יותר מדי כמובן, אבל גם לא פחות מדי.
נ.ב. 1 עד היום עבדתי עם ווידג'ט של תגובות של Disqus שעבד בחינם ובגדול בסדר. לצערי קיבלתי מהם הודעה שהם החליטו להוסיף פירסומות לווידג'ט לכל הלקוחות החינמיים ולכן הסרתי אותם מהאתר. לגירסה הבאה כבר אכתוב משהו בעצמי או אשתמש בספריית קוד פתוח. בינתיים מוזמנים לכתוב תגובות במייל או בטלגרם.
נ.ב. 2 יש לכם רעיון לפרויקט ולא בטוחים איך להתחיל? התחלתם לעבוד על פרויקט אבל לא מצליחים להגיע לפרודקשן? אני אשמח לעזור. במרץ אני פותח תוכנית ליווי לבניית פרויקטים. אם מעניין אתכם להצטרף לחצו כאן ונתאם שיחת היכרות.
1 419
הזמנה לסדנה - מנטורינג לבניית פרויקט
פרויקטי תוכנה יכולים לשנות את העולם. רבים מהם כבר עשו את זה. ומה שמדהים בפרויקטי תוכנה זה שההתחלה שלהם יכולה להיות מאוד מהירה: ג'ימייל, וורדל, טוויטר, פלאפי בירד, גיט ופרויקטים רבים נוספים החלו את דרכם בכמה ימים או שבועות של פיתוח שהוביל לפרוטוטייפ והשאר היסטוריה. וכל זה היה לפני עידן ה AI.
הכלים שיש לנו היום לבניית מערכות תוכנה, ובמיוחד החיבור המהיר ל AI, הם טובים בהרבה מכל מה שאפשר היה לחלום עליו עד לפני מספר שנים. אינסוף פרויקטים שפעם היו מסובכים מדי למימוש או יקרים מדי הפכו ריאליים, ואינסוף עבודה שפעם לקחה המון זמן יכולה להתבצע היום במהירות על ידי AI. אז מה עדיין עוצר אותנו?
האמת שלא מעט דברים:
1. אין לי זמן להשקיע בפרויקט עכשיו.
2. אין לי רעיון טוב.
3. יש לי יותר מדי רעיונות.
4. אין לי מושג איך מתחילים.
5. חבל על הזמן, ממילא לא יצא מזה כלום.
הכל נכון אבל במקביל אנחנו גם יודעים שתיק עבודות הוא אחד הדברים הכי חשובים כשאנחנו באים להתראיין בתור מפתחים. אנחנו יודעים שרק דרך התנסות אמיתית בעבודה על פרויקט אנחנו באמת יכולים ללמוד ולהבין טכנולוגיה. אנחנו יודעים שעם כל הכבוד לעבודה בצוות במקום עבודה, יש ערך עצום בבניית פרויקט מאפס בעצמך.
בתחילת מרץ אני מתכוון לפתוח קבוצת מנטורינג לעבודה על פרויקט Full Stack במשך ארבעה שבועות ובאמצעות שילוב כלי AI עם כל הסטאק הטכנולוגי שאנחנו כבר מכירים. העבודה בקבוצה תתן לכם את המקום והכלים ליצור פרויקט מאפס ועד פרודקשן, ואתם לא לבד: אנחנו נקיים מפגשים קבוצתיים בזום בהם נלמד אחד מהשני ונציג את העבודה שעשינו, נחזיק קבוצת דיונים בווטסאפ ותקבלו פגישות אחד על אחד איתי לייעוץ טכנולוגי ו Code Review ספציפי על הפרויקט. העבודה במסגרת קבוצתית ועם מטרה ברורה של בניית פרויקט תתן גם לכם את המוטיבציה להתאמץ יותר ולהגיע לקו הסיום עם פרויקט בפרודקשן.
המחזור הראשון יהיה מוגבל ל 8 אנשים כדי להבטיח הצלחה ותשומת לב אישית לכל אחד ואחת. אם אתם בעניין ורוצים כבר לשריין מקום או לשמוע עוד פרטים תשאירו כבר הודעה ולא תצטרכו לעמוד בתור. אפשר לענות ישירות למייל או להשאיר הודעה באתר בקישור:
https://www.tocode.co.il/contacts/new
1 419
ניהול טפסים בריאקט עם Formik ו Yup
טפסים מדכאים אותי. באמת. לאורך זמן אנחנו רק מוסיפים להם פונקציונאליות והופכים אותם ליותר מסובכים, שדות מסוימים מופיעים ואחרים נעלמים לפי בחירות קודמות בטופס, מה שאומר שבריאקט טפסים מובילים להמון משתני סטייט. ספריית פורמיק מציעה דרך קלה לצמצם את מספר משתני הסטייט, ובשילוב עם yup גם קל לנו (יחסית) לארגן את הקוד ולהפריד בין הלוגיקה של הוולידציה למראה של הטופס. בואו נראה איך זה עובד.
קוד הוולידציה
אני מתקין את שתי הספריות עם:
npm install formik yup
ואז יוצר תיקייה חדשה בשם schema ובתוכה קובץ לוולידציית טופס ראשונה, אני קורא לו LoginForm.ts. זה תוכן הקובץ:
import * as Yup from 'yup';
export default Yup.object().shape({
email: Yup.string()
.email('Invalid email format')
.required('Email is required'),
password: Yup.string()
.required('Password is required'),
rememberMe: Yup.boolean().default(false),
});
הקובץ מתאר את הציפיות שלי מהערכים בטופס ואת הודעות השגיאה שאני רוצה להציג אם דברים לא תואמים לסכימה. אפשר גם להוסיף לכל שדה וולידציות משלכם בתור פונקציות אסינכרוניות, וכך למשל טופס רישום יכול לוודא שכתובת המייל לא רשומה כבר למערכת. אפשר להגדיר גם קשר בין שדות כדי לוודא ששדה אימות סיסמה באמת מכיל את אותו טקסט כמו הסיסמה או שבחירה בשדה מסוים דורשת בחירה גם בשדה אחר. כתבתי על yup בפוסטים אחרים ועוד אכתוב עליו, אבל באמת זו ספריה עם הרבה יכולות ושווה ללמוד עליה.
קוד הטופס
בצד של הטופס יש ממשק מאוד פשוט בין פורמיק ל yup וזה נראה כך:
import { useFormik } from 'formik';
import loginFormSchema from './schema/LoginForm';
export default () => {
const formik = useFormik({
initialValues: {email: '', password: '', rememberMe: false},
validationSchema: loginFormSchema,
onSubmit: values => {
alert(JSON.stringify(values, null, 2));
},
});
return (
<form onSubmit={formik.handleSubmit}>
<div>
<label>
Email
<input
name="email"
type="email"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email ? (
<div>{formik.errors.email}</div>
) : null}
</label>
</div>
<div>
<label>
Password:
<input
name="password"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
/>
{formik.touched.password && formik.errors.password ? (
<div>{formik.errors.password}</div>
) : null}
</label>
</div>
<div>
<label>
<input
type="checkbox"
name="rememberMe"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
checked={formik.values.rememberMe}
/>
Remember Me
</label>
</div>
<button type="submit">Login</button>
</form>
);
};
הפקודה שמחברת בין הטופס לסכימה היא:
validationSchema: loginFormSchema,
והבלוק הזה אחרי כל input אחראי על הצגת הודעות השגיאה ליד השדה:
{formik.touched.email && formik.errors.email ? (
<div>{formik.errors.email}</div>
) : null}1 419
* can become safe by removing exactly one level. *
def is_safe_with_dampener?(levels)
# If already safe, no need to remove anything.
return true if is_safe?(levels)
# Otherwise, try removing each level in turn.
(0...levels.size).each do |idx|
new_levels = levels[0...idx] + levels[idx+1..-1]
return true if is_safe?(new_levels)
end
false
end
* --- Read the input --- *
* Typically you'd read from a file or from standard input. *
* For Advent of Code, you might do something like: *
* lines = File.readlines("input.txt").map(&:strip) *
* Here we'll read from standard input (ARGF) to keep it general. *
lines = ARGF.each_line.map(&:strip)
* Convert each line of space-separated numbers into an integer array. *
reports = lines.map { |line| line.split.map(&:to_i) }
* --- Part 1: Count how many reports are safe under the original rules --- *
part1_safe_count = reports.count { |levels| is_safe?(levels) }
* --- Part 2: Count how many reports are safe with the "Problem Dampener" --- *
part2_safe_count = reports.count { |levels| is_safe_with_dampener?(levels) }
* Output the answers *
puts "Part 1 (original rules) safe reports: #{part1_safe_count}"
puts "Part 2 (with Problem Dampener) safe reports: #{part2_safe_count}"
מבחינת הקוד אין פה הברקות. אישית אני לא משתמש ב return בתוך בלוקים שעוברים לפונקציות, וגם אני לא חושב שיש צורך בכל כך הרבה הערות, אבל עדיין צריך להוריד את הכובע. העובדה שאפשר להמשיך את השיחה כדי לגלות בעיות ולשפר את הקוד יכולה לשפר משמעותית גם את חווית הלימוד.1 419
פיתרון Advent Of Code 2024 יום 2 ב Ruby
כן אני אוהב לפתור חידות תכנות והחידות של Advent Of Code, מצליחות הרבה פעמים להפתיע, למרות המלל הרב. בפוסט זה נראה פיתרון לא מלהיב מדי של יום 2 מהסט האחרון ברובי ואנסה לדמיין איך היה נראה פיתרון יעיל יותר.
האתגר
נתון קלט שבנוי מרשימות של מספרים:
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
והאתגר מחולק לשני חלקים. בחלק הראשון צריך למצוא כמה מהרשימות האלה מתאימות לסט התנאים הבא:
1. או שהרשימה רק עולה או רק יורדת.
2. קצב העלייה או הירידה לא גבוה מ-3.
בחלק השני עלינו לדמיין שאפשר למחוק מספר אחד מהרשימה ולראות אם עכשיו הרשימה תתאים לתנאי.
פיתרון חלק 1
החלק הראשון היה די פשוט והמפתח לפיתרון הוא השורה הבאה:
line.split.map(&:to_i).each_cons(2).map { _2 - _1 }
אני לוקח שורה, חותך אותה למספרים, הופך כל מספר ל integer ואז רץ על כל הזוגות ברשימה ומחשב את ההפרש ביניהם. סך הכל אם הרשימה התחילה כך:
line = '7 6 4 2 1'
אז שורת הקריאה תחזיר לי:
3.3.5 :021 > line.split.map(&:to_i).each_cons(2).map { _2 - _1 }
=> [-1, -2, -2, -1]
אחרי שיש את זה קל לרוץ על ההפרשים ולבדוק שהכל מתאים לתנאים, וסך הכל הפיתרון:
filename = ARGV[0]
parsed_input = File.foreach(filename).map do |line|
line.split.map(&:to_i).each_cons(2).map { _2 - _1 }
end
safe = parsed_input.filter do |seq|
(seq.all?(&:positive?) || seq.all?(&:negative?)) && (seq.all? { it.abs <= 3 })
end
pp safe.count
פיתרון חלק 2
בחלק השני היה צריך לזהות אם אפשר למחוק איבר אחד מהרשימה ולקבל רשימה תקינה. הדרך הקלה לגשת לזה היתה פשוט לנסות למחוק איברים ולראות אם יש מישהו שבלעדיו הרשימה תקינה. זה הקוד:
filename = ARGV[0]
def check_seq(seq)
diff_seq = seq.each_cons(2).map { _2 - _1 }
(diff_seq.all?(&:positive?) || diff_seq.all?(&:negative?)) && (diff_seq.all? { it.abs <= 3 })
end
parsed_input = File.foreach(filename).map do |line|
line.split.map(&:to_i)
end
safe = parsed_input.filter do |seq|
check_seq(seq) ||
seq.each_index.any? do |i|
check_seq(seq.reject.with_index { _2 == i })
end
end
pp safe.count
בשביל למחוק אינדקס מרשימה ברובי השתמשתי בשורה:
[2, 3, 5].reject.with_index { _2 == 1 }
וכן זה עובד אבל אני מודה שלא שימח אותי במיוחד בגלל שהוא עושה הרבה עבודה כפולה. אם נדמיין את הרשימה הבאה:
line = '2 3 4 5 6 7 8 9 10 11 12 100'
אז ברור שתחילת הרשימה תקינה והמספר 100 בסוף הוא הבעייתי. אין טעם לנסות למחוק את כל האינדקסים בהתחלה כי אנחנו כבר יודעים שגם איתם הכל בסדר, ואפשר מראש למחוק רק את האינדקס הראשון שמכיל הפרה. אני חושב שזה יהיה שיפור טוב לתוכנית אבל מודה שלא הספקתי לכתוב אותו כי הוא קצת יאריך את הקוד.
פיתרון של Chat GPT
המטרה של Advent Of Code אף פעם לא היתה לפתור את התרגיל בשביל שיהיה לך את קוד הפיתרון (אנשים תמיד פרסמו את התשובות שלהם ברשת ואין בעיה להעתיק), אלא בשביל האתגר האישי והחוויה הלימודית. מסיבה זו אני לא חושב שזה משנה ש Chat GPT יכול לפתור את התרגילים בקלות. ובכל זאת יש משהו קסום ביכולת להדביק דף שאלה ל Chat GPT ולקבל קוד שעובד. זה הקוד שקיבלתי ממנו:
* !/usr/bin/env ruby *
* --- Helpers --- *
* Returns true if the array of levels is strictly increasing or strictly decreasing *
* with each adjacent difference in the range [1..3]. *
def is_safe?(levels)
return true if levels.size < 2 # Trivially safe if 0 or 1 level
# We check both possibilities (strictly increasing or strictly decreasing).
is_increasing = true
is_decreasing = true
(0...levels.size-1).each do |i|
diff = levels[i+1] - levels[i]
# Must be in [-3..-1] or [1..3]; if it's 0 or outside ±3 => not safe
return false if diff.abs < 1 || diff.abs > 3
# If it's not strictly positive, can't be strictly increasing
is_increasing = false if diff <= 0
# If it's not strictly negative, can't be strictly decreasing
is_decreasing = false if diff >= 0
end
# Safe if either strictly increasing or strictly decreasing
is_increasing || is_decreasing
end
* Returns true if the array of levels is safe as-is OR *1 419
<p>{lastName}</p>
<button onClick={swap}>Swap</button>
<Link href="/">Counter</Link>
</div>
)
}
הפונקציה useNamesSelector מוגדרת בסלייס של names ומתאימה ל State של דף השמות. טייפסקריפט מספיק חכם בשביל להבין שה State בתוך פונקציה זו הוא מסוג Names, וכך יש לי השלמה אוטומטית ובדיקת טיפוסים.
חיבור ל next
נשאר לנו רק לחבר את הקוד ל next. אני משתמש ב Pages Router. בקובץ _app.tsx אני כותב:
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { makeStore } from '../redux/store'
import { Provider } from 'react-redux'
export default function App({ Component, pageProps }: AppProps) {
const store = makeStore(pageProps);
return <Provider store={store}>
<Component />
</Provider>
}
וכך יוצר את ה store מתוך ה pageProps, שזה אוביקט המידע שהשרת הכין. בכל אחד מהדפים אני מאתחל את ה pageProps, לדוגמה דף השמות נראה כך:
import Image from 'next/image'
import Names from '@/components/Names';
export const getServerSideProps = (async () => {
return { props: {firstName: 'first', lastName: 'last'} };
});
export default function Home() {
return (
<main>
<Names />
</main>
)
}
הפונקציה getServerSideProps מאתחלת את אוביקט המידע, נקסט יעביר אותו ל App ושם ייווצר ה Store וכל מעבר דף ה Store יאותחל עם המידע החדש שמגיע מהשרת.1 419
דוגמת קוד: שילוב Redux עם Next.js
שילוב בין next.js ל Redux יכול להיות מבלבל כי כל אחת משתי הספריות רוצה לעשות משהו שהספריה השנייה עושה - בדוגמאות של next הם שמחים להראות איך עובדים רק עם next ומנהלים סטייט לגמרי בתוך העולם שלהם, ובדוגמאות של רידאקס הם שמחים להראות איך לכתוב store לעמוד אחד אבל כשצריך להתמודד עם מספר עמודים וסטייט בצד שרת יותר קשה למצוא דוגמאות.
בפוסט זה אני מציג רעיון אחד לשילוב בין הספריות שמבוסס על העקרונות הבאים:
1. קוד צד שרת מכין אוביקט מידע (דרך שליפה מבסיס הנתונים, גישה לשירותי צד שלישי או כל מנגנון אחר).
2. אוביקט המידע הזה הופך ל Store של רידאקס ונשלח לצד הלקוח.
3. קומפוננטות שקוראות מרידאקס צריכות לבחור מה העמוד הראשי שהן מצפות להיות בו, כדי שנדע איזה State Object הן צפויות לקבל.
קוד ה Server Side Rendering יכול לעבוד עם אוביקט המידע שהשרת הכין וקוד צד לקוח יכול לעשות dispatch ל Actions לתוך אוביקט המידע הזה.
הריפו של הדוגמה זמין כאן:
https://github.com/ynonp/next-pages-redux
בואו נראה איך זה עובד.
קובץ ה store
תחנה ראשונה היא הקובץ store.ts. מה שחשוב כאן הוא להגדיר פונקציה בשם
makeStore שמקבלת את הסטייט הראשוני ומחזירה store שמתאימה לו. אני גם הוספתי תמיכה בפעולת set שכותבת ערך לאיזשהו שדה ב store:
import { configureStore, createReducer } from '@reduxjs/toolkit'
import { set } from './actions';
export function makeStore(initialState: any) {
return configureStore({
reducer: createReducer(initialState, builder => {
builder.addCase(set, (state, action) => {
// @ts-ignore
state[action.payload.key] = action.payload.value;
})
})
})
}
// Infer the \RootState\ and \AppDispatch\ types from the store itself
export type TState = ReturnType<typeof makeStore>;
export type RootState = ReturnType<TState['getState']>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = TState['dispatch']
קבצי הדפים
בפרויקט הדוגמה יש שני דפים: counter ו names. לכל דף יש אוביקט סטייט ראשי שונה, רק אחרי הפעלת makeStore נדע מי אוביקט הסטייט שבו אנחנו משתמשים. זה הקוד של counter.ts:
import {useSelector, TypedUseSelectorHook} from 'react-redux';
export type Counter = {
count: number;
}
export const initialState: Counter = {
count: 0,
}
export const useCounterSelector: TypedUseSelectorHook<Counter> = useSelector;
וזה הקוד של names.ts:
import {useSelector, TypedUseSelectorHook} from 'react-redux';
import { createAction } from '@reduxjs/toolkit';
import type {SetFieldPayload} from './types';
export type Names = {
firstName: string,
lastName: string,
}
export const initialState: Names = {
firstName: 'a',
lastName: 'b',
}
export const useNamesSelector: TypedUseSelectorHook<Names> = useSelector;
נשים לב שבפרויקט הדוגמה בחרתי להשתמש ב Action יחיד שיתאים לכל הדפים, אבל בהחלט אפשר היה לדמיין שכל דף יהיה slice עם actions משלו. במצב כזה הפונקציה makeStore היתה מקבלת Reducer במקום רק initial state.
הקומפוננטות
קומפוננטות שונות יכולות להיכנס לדפים שונים, כי הן פונות לנתיבים אחרים בסטייט. יותר מזה כשאני נכנס לדף מסוים רק הפריטים של דף זה בכלל זמינים בתוך הסטייט, כלומר אני לא יכול להציג קומפוננטה שפונה ל names בתוך דף ה counter ולהיפך.
בשביל לציין את זה בקוד הקומפוננטה הגדרתי פונקציית useSelector שונה לכל דף, וכך קומפוננטת ה names משתמשת ב selector שמתאים לדף שלה, כלומר:
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { set } from '../redux/actions';
import Link from 'next/link'
import { useNamesSelector } from '@/redux/pages/names';
export default function() {
const dispatch = useDispatch();
const firstName = useNamesSelector(s => s.firstName);
const lastName = useNamesSelector(s => s.lastName);
function swap() {
dispatch(set({key: 'firstName', value: lastName}))
dispatch(set({key: 'lastName', value: firstName}))
}
return (
<div>
<p>{firstName}</p>
¡Ya disponible! Investigación de Telegram 2025 — los principales insights del año 
