ToCode
رفتن به کانال در Telegram
1 418
مشترکین
-224 ساعت
-37 روز
-630 روز
آرشیو پست ها
1 418
הזמנה לסדנה - מנטורינג לבניית פרויקט
פרויקטי תוכנה יכולים לשנות את העולם. רבים מהם כבר עשו את זה. ומה שמדהים בפרויקטי תוכנה זה שההתחלה שלהם יכולה להיות מאוד מהירה: ג'ימייל, וורדל, טוויטר, פלאפי בירד, גיט ופרויקטים רבים נוספים החלו את דרכם בכמה ימים או שבועות של פיתוח שהוביל לפרוטוטייפ והשאר היסטוריה. וכל זה היה לפני עידן ה AI.
הכלים שיש לנו היום לבניית מערכות תוכנה, ובמיוחד החיבור המהיר ל AI, הם טובים בהרבה מכל מה שאפשר היה לחלום עליו עד לפני מספר שנים. אינסוף פרויקטים שפעם היו מסובכים מדי למימוש או יקרים מדי הפכו ריאליים, ואינסוף עבודה שפעם לקחה המון זמן יכולה להתבצע היום במהירות על ידי AI. אז מה עדיין עוצר אותנו?
האמת שלא מעט דברים:
1. אין לי זמן להשקיע בפרויקט עכשיו.
2. אין לי רעיון טוב.
3. יש לי יותר מדי רעיונות.
4. אין לי מושג איך מתחילים.
5. חבל על הזמן, ממילא לא יצא מזה כלום.
הכל נכון אבל במקביל אנחנו גם יודעים שתיק עבודות הוא אחד הדברים הכי חשובים כשאנחנו באים להתראיין בתור מפתחים. אנחנו יודעים שרק דרך התנסות אמיתית בעבודה על פרויקט אנחנו באמת יכולים ללמוד ולהבין טכנולוגיה. אנחנו יודעים שעם כל הכבוד לעבודה בצוות במקום עבודה, יש ערך עצום בבניית פרויקט מאפס בעצמך.
בתחילת מרץ אני מתכוון לפתוח קבוצת מנטורינג לעבודה על פרויקט Full Stack במשך ארבעה שבועות ובאמצעות שילוב כלי AI עם כל הסטאק הטכנולוגי שאנחנו כבר מכירים. העבודה בקבוצה תתן לכם את המקום והכלים ליצור פרויקט מאפס ועד פרודקשן, ואתם לא לבד: אנחנו נקיים מפגשים קבוצתיים בזום בהם נלמד אחד מהשני ונציג את העבודה שעשינו, נחזיק קבוצת דיונים בווטסאפ ותקבלו פגישות אחד על אחד איתי לייעוץ טכנולוגי ו Code Review ספציפי על הפרויקט. העבודה במסגרת קבוצתית ועם מטרה ברורה של בניית פרויקט תתן גם לכם את המוטיבציה להתאמץ יותר ולהגיע לקו הסיום עם פרויקט בפרודקשן.
המחזור הראשון יהיה מוגבל ל 8 אנשים כדי להבטיח הצלחה ותשומת לב אישית לכל אחד ואחת. אם אתם בעניין ורוצים כבר לשריין מקום או לשמוע עוד פרטים תשאירו כבר הודעה ולא תצטרכו לעמוד בתור. אפשר לענות ישירות למייל או להשאיר הודעה באתר בקישור:
https://www.tocode.co.il/contacts/new
1 418
ניהול טפסים בריאקט עם 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 418
* 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 418
פיתרון 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 418
<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 418
דוגמת קוד: שילוב 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>1 418
למה קשה לחשב שברים
אבי בר-אלי מתלונן בדה מרקר על מחשבי הביטוח הלאומי שלא יודעים לחשב את מס הבריאות החדש כי הוא מכיל שלוש ספרות אחרי הנקודה העשרונית. כל מי שקצת מבין במחשבים עלול להרגיש מרומה כאן: מה אכפת למחשב כמה ספרות יש אחרי הנקודה העשרונית? ולמה הביטוח הלאומי מעריך את השינוי במיליון וחצי ש"ח? אז אני אומנם לא יודע כלום על מערכות הביטוח הלאומי אבל בפוסט הזה אנסה לעשות תרגיל מחשבתי כדי להבין למה זה מסובך.
איך מחשבים שברים
כל שפת תכנות כולל הישנות יודעת לבצע חישובים. לדוגמה בשפת פייתון אנחנו יכולים לכתוב את הקוד הבא כדי להדפיס את המכפלה של 2 ו-3 ולקבל על המסך 6:
x = 3
y = 2
print(x * y)
הבעיה של מחשבים מתחילה כשצריך לחשב שברים. מסיבות היסטוריות וטכניות טובות (אפשר לקרוא עליהן בויקיפדיה) רוב שפות התכנות כוללות בעיות דיוק בעבודה עם שברים, לדוגמה באותו פייתון אם אני כותב:
x = 0.1
y = 0.2
print(x + y)
אני אקבל את התוצאה:
0.30000000000000004
ולא את 0.3 כמו שאולי אפשר היה לצפות. או בדוגמה נוספת:
>>> 2 - 1.1
0.8999999999999999
יש מצבים בהם אנחנו מוכנים לחיות עם אי דיוקים כאלה, ומצבים אחרים בהם אנחנו מעדיפים למצוא דרך אחרת לשמור את המידע שלנו כדי לא ליפול במלכודות ובאי דיוקים.
מה עושים עם הכסף
אחד המקומות בהם טעות זו יכולה להיות הכי מרגיזה הוא בעבודה עם כסף. ברור שאם מישהו קונה מוצר שעולה 2 ש"ח ומשלם שקל ועשר אגורות אנחנו צריכים להחזיר לו 90 אגורות, אפילו שהתרגיל בתוכנית המחשב לא משקף את זה.
מסיבה זו טריק נפוץ לעבודה עם שברים שיעזור לנו לשמור את הדיוק הוא להפוך אותם למספרים שלמים, לדוגמה בעבודה עם כסף אנחנו יכולים פשוט לשמור את המספר באגורות. בסיפור כזה התרגיל של אותו לקוח שקנה בשקל ועשרה מוצר שעולה שני שקלים יראה כך:
>>> 200 - 110
90
וכולם מבינים פה שצריך להחזיר 90 אגורות.
אז מה קרה לביטוח הלאומי? אני לא יודע. אבל אולי הם השתמשו בטריק דומה - נניח שהם שמרו בבסיס הנתונים ובקוד את כל המספרים מוכפלים ב 100 כדי לא לעבוד עם שברים, ורק בהדפסה הציגו את המספר כאילו מדובר על שבר עם שתי ספרות אחרי הנקודה העשרונית. אם זה מה שהם עשו, ברור למה יהיה מסובך לתקן את זה ל-3 ספרות:
1. עליהם למצוא את כל המידע בבסיס הנתונים ששמור בפורמט שהם יצרו ולעדכן את הפורמט ל-3 ספרות אחרי הנקודה העשרונית.
2. עליהם למצוא את כל המקומות בקוד בהם מוגדרים מספרים בפורמט הזה ולתקן ל-3 ספרות.
3. עליהם למצוא את כל קטעי הקוד שקוראים מידע או כותבים אותו ולתקן אותם כדי לכפול באלף במקום במאה.
וכן זה המון עבודה ובמערכות ישנות גם פוטנציאל גבוה לטעויות.
מה עושים במקום? כרגע לא הרבה. יש דברים שפשוט קשה לשנות אותם. כן כדאי לזכור כשאנחנו בונים מערכות חדשות להיזהר מהסקת מסקנות כלליות על העולם מהנתונים שיש לנו, כלומר גם אם עכשיו כל הנתונים שלנו נראים כאילו אחוזים יכולים לכלול רק שתי ספרות אחרי הנקודה העשרונית, מגבלה זו אינה הכרחית למושג האחוז ובהחלט אפשר לדמיין אחוזים קטנים יותר. אלטרנטיבה שלפעמים יכולה להיות טובה יותר היא להשתמש במספרים דצימליים גדולים (קיימים בשפות תכנות מודרניות) שם החישוב יותר מדויק. בפייתון יש לנו:
from decimal import *
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')
שימו לב שהקפתי את 0.1 ו 0.2 בסימן של מחרוזת, כדי שפייתון לא ישמור אותם בטעות בייצוג הנקודה העשרונית.1 418
ניסוי ריאקט: משתנה סטייט לקריאה וכתיבה
ב Vue אנחנו יוצרים משתנה סטייט עם הפקודה ref ומקבלים משהו שאפשר לכתוב אליו וגם לקרוא ממנו לדוגמה הקוד הבא מציג מונה לחיצות עם משתנה סטייט ששומר את מספר הלחיצות:
<script setup>
import { ref } from 'vue'
const counter = ref(0)
function inc() { counter.value++ }
</script>
<template>
<p>{{counter}}</p>
<button @click="inc">+1</button>
</template>
בריאקט הפקודה useState מחזירה שני דברים: את הערך עצמו של משתנה הסטייט ופונקציה שמעדכנת אותו. בואו ננסה לעטוף את useState כדי לקבל רק דבר אחד שיתנהג כמו משתנה סטייט ב vue.
פיתרון: useRefState
בשביל לקבל רק אוביקט אחד שמאפשר כתיבה וקריאה אני משתמש בפרוקסי. הפרוקסי יעטוף את האוביקט ויתפוס כתיבות לשדה value שלו, ואז יחליף כל כתיבה לשדה value בהפעלת ה setter. כך נראה הקוד:
function toRef(value, setter) {
return new Proxy(
{ value },
{
get(target, prop, receiver) {
return value;
},
set(obj, prop, value) {
setter(value);
},
}
);
}
function useRefState(initialValue) {
const [value, setter] = useState(initialValue);
return toRef(value, setter);
}
בתוך קומפוננטה אפשר להשתמש במנגנון בדיוק כמו ב Vue:
export default function App() {
const counter = useRefState(0);
function inc() {
counter.value++;
}
return (
<div className="App">
<p>{counter.value}</p>
<button onClick={inc}>+1</button>
<button onClick={() => counter.value--}>-1</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
הפונקציה inc מעלה ב-1 את ה value של counter, וזה מספיק בשביל לגרום לרינדור מחדש של הקומפוננטה. הצגת ה value מתוך התבנית מראה את הערך שלו.
עדיין יש מספר פערים בפיתרון:
1. ב Vue אפשר להשתמש ב counter ישירות בתוך התבנית. ריאקט לא מרשה לי לכתוב אובייקטים בתוך התבנית ולכן חייבים להשתמש ב .value גם שם.
2. ומה שיותר משמעותי, ב Vue רק קריאה ממשתנה ריאקטיבי יוצרת מנוי לערך של אותו משתנה. קומפוננטה שלא קוראת ממשתנה ריאקטיבי לא תתרנדר מחדש כשאותו משתנה יתעדכן. בריאקט השינויים קורים ברזולוציה של קומפוננטה ולכן מספיק שיש עדכון למשתנה סטייט שהוגדר בקומפוננטה מסוימת כדי שקומפוננטה זו תתרנדר מחדש.
נ.ב. בתור קונספט בשביל ליצור משתנה סטייט של "דבר אחד" אני מעדיף להשתמש בפונקציה. הפעלה של הפונקציה בלי פרמטרים מחזירה את הערך הנוכחי והפעלה עם פרמטרים תעדכן, כלומר:
function useRefState(initialValue) {
const [value, setter] = useState(initialValue);
return function (...args) {
if (args.length > 0) {
setter(...args);
} else {
return value;
}
};
}
export default function App() {
const counter = useRefState(0);
function inc() {
counter((c) => c + 1);
}
return (
<div className="App">
<p>{counter()}</p>
<button onClick={inc}>+1</button>
<button onClick={() => counter((c) => c - 1)}>-1</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}1 418
ניסוי ריאקט: משתנה סטייט לקריאה וכתיבה
ב Vue אנחנו יוצרים משתנה סטייט עם הפקודה ref ומקבלים משהו שאפשר לכתוב אליו וגם לקרוא ממנו לדוגמה הקוד הבא מציג מונה לחיצות עם משתנה סטייט ששומר את מספר הלחיצות:
<script setup>
import { ref } from 'vue'
const counter = ref(0)
function inc() { counter.value++ }
</script>
<template>
<p>{{counter}}</p>
<button @click="inc">+1</button>
</template>
בריאקט הפקודה useState מחזירה שני דברים: את הערך עצמו של משתנה הסטייט ופונקציה שמעדכנת אותו. בואו ננסה לעטוף את useState כדי לקבל רק דבר אחד שיתנהג כמו משתנה סטייט ב vue.
פיתרון: useRefState
בשביל לקבל רק אוביקט אחד שמאפשר כתיבה וקריאה אני משתמש בפרוקסי. הפרוקסי יעטוף את האוביקט ויתפוס כתיבות לשדה value שלו, ואז יחליף כל כתיבה לשדה value בהפעלת ה setter. כך נראה הקוד:
function toRef(value, setter) {
return new Proxy(
{ value },
{
get(target, prop, receiver) {
return value;
},
set(obj, prop, value) {
setter(value);
},
}
);
}
function useRefState(initialValue) {
const [value, setter] = useState(initialValue);
return toRef(value, setter);
}
בתוך קומפוננטה אפשר להשתמש במנגנון בדיוק כמו ב Vue:
export default function App() {
const counter = useRefState(0);
function inc() {
counter.value++;
}
return (
<div className="App">
<p>{counter.value}</p>
<button onClick={inc}>+1</button>
<button onClick={() => counter.value--}>-1</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
הפונקציה inc מעלה ב-1 את ה value של counter, וזה מספיק בשביל לגרום לרינדור מחדש של הקומפוננטה. הצגת ה value מתוך התבנית מראה את הערך שלו.
עדיין יש מספר פערים בפיתרון:
1. ב Vue אפשר להשתמש ב counter ישירות בתוך התבנית. ריאקט לא מרשה לי לכתוב אובייקטים בתוך התבנית ולכן חייבים להשתמש ב .value גם שם.
2. ומה שיותר משמעותי, ב Vue רק קריאה ממשתנה ריאקטיבי יוצרת מנוי לערך של אותו משתנה. קומפוננטה שלא קוראת ממשתנה ריאקטיבי לא תתרנדר מחדש כשאותו משתנה יתעדכן. בריאקט השינויים קורים ברזולוציה של קומפוננטה ולכן מספיק שיש עדכון למשתנה סטייט שהוגדר בקומפוננטה מסוימת כדי שקומפוננטה זו תתרנדר מחדש.
נ.ב. בתור קונספט בשביל ליצור משתנה סטייט של "דבר אחד" אני מעדיף להשתמש בפונקציה. הפעלה של הפונקציה בלי פרמטרים מחזירה את הערך הנוכחי והפעלה עם פרמטרים תעדכן, כלומר:
function useRefState(initialValue) {
const [value, setter] = useState(initialValue);
return function (...args) {
if (args.length > 0) {
setter(...args);
} else {
return value;
}
};
}
export default function App() {
const counter = useRefState(0);
function inc() {
counter((c) => c + 1);
}
return (
<div className="App">
<p>{counter()}</p>
<button onClick={inc}>+1</button>
<button onClick={() => counter((c) => c - 1)}>-1</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}1 418
תרגיל טייפסקריפט: פעולת עדכון גנרית ב Redux
בדוגמת ההתחלה המהירה של Redux Toolkit הם מציעים את הקוד הבא עבור סלייס של מונה:
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
לא מפתיע שזה מתאים בדיוק לתפיסת העולם של Redux - מעט מידע, שניתן לשינוי במספר דרכים (פעולות), כל דרך שינוי עם המגבלות והבדיקות שלה. אבל בעולם האמיתי יש עוד מקרה נפוץ שלא מדברים עליו בתיעוד שלהם, וזה אוביקט עם הרבה מידע שמאפשר שינויים יחסית גמישים. דמיינו את אוביקט המידע הבא:
export interface CounterSliceState {
value: number
status: "idle" | "loading" | "failed"
foo: string
bar: number
buz: Array<string>
}
ואולי יהיו שם עוד 20 שדות מטיפוסים שונים. כתבו פעולת Set גנרית (אחת) שתקבל מפתח וערך מהסוג שמתאים לו ותכתוב את זה לאוביקט המידע.
פיתרון
בשביל הפיתרון עלינו תחילה לכתוב את ה Reducer. אפשר להגדיר משהו כזה:
extraReducers: (builder) => {
builder.addCase(setField, (state, action) => {
const { key, value } = action.payload;
// @ts-ignore
state[key] = value;
});
},
אני מוכן להתעלם כאן מבעיות טייפסקריפט כי הוולידציה מתרחשת קודם, בעת יצירת ה Action. הקוד הבא יוצר Action גנרי עבור כתיבה לשדה כלשהו באוביקט מידע:
type SetFieldPayload = {
[K in keyof CounterSliceState]: {
key: K;
value: CounterSliceState[K];
};
}[keyof CounterSliceState];
export const setField = createAction<SetFieldPayload2>('counter/set');
ועכשיו הקוד הזה מתקמפל:
setField({key: 'bar', value: 5}) // compiles OK
אבל זה זורק שגיאה:
setField({key: 'bar', value: '?'}) // compilation error
נ.ב. אפשר להרחיב את הפיתרון ולכתוב גם למפתח מקונן (בקשו מ Chat GPT את הקוד אם לא מצאתם את הרקורסיה), אבל אני לא ממליץ. ככל שטייפסקריפט נהיה מסובך מדי הוא מפסיק להיות שימושי, ובאופן כללי רידאקס עובד יותר טוב עם סלייסים שטוחים. יותר קל לקחת את החלק המקונן ולהפריד אותו לסלייס משלו.1 418
מה מודדים
בהתלבטות בין שני קורסים יש דברים שמאוד קל למדוד: אפשר להשוות את אורך הקורסים, את המחיר, את המחיר לשעה, אפשר להסתכל על עבודות של בוגרים אחרים ולמדוד כמה הפרויקט נראה מעניין ואפילו אפשר להשוות כמה תארים או כמה שנות ניסיון יש למרצים.
אבל את הדברים שחשוב למדוד הרבה יותר קשה לזהות: חשוב למדוד כמה מוטיבציה הקורס ייתן לי כדי להמשיך ללמוד גם אחריו. חשוב למדוד כמה מהר הקורס יאפשר לי להתחיל לעבוד לבד, וכמה רחוק אוכל להגיע בעזרת הרעיונות והעקרונות שאלמד שם. חשוב למדוד כמה גבוה הסיכוי שלי למצוא עבודה אחרי הקורס והאם המרצים באמת יוכלו להבין את המצב שלי ולתת לי עצות רלוונטיות עבורי.
בשביל למדוד את הדברים החשובים לא מספיק להסתכל בסילבוס - צריך ממש לדבר עם המרצים ועם בוגרים, להשתתף בשיעורי ניסיון ואולי אפילו ללמוד מראש בבית כדי להגיע עם מספיק רקע ולשאול את השאלות הנכונות.
רק בגלל שקל למדוד משהו לא אומר שהוא נותן אינדיקציה טובה להצלחה.
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
