הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

תבנית פרויקט: next, drizzle, auth0

26/01/2025

רוצים להתחיל פרויקט React חדש שיתחבר לבסיס נתונים ויכיל מנגנון ניהול משתמשים מובנה? ברור, מי לא ירצה. במקום לשבור את השיניים על חיבור כל הכלים תנו לי לחסוך לכם עבודה ולשתף תבנית פשוטה שכתבתי שפשוט עובדת, מבוססת על דוגמה של auth0. זה הגיטהאב:

https://github.com/ynonp/next-drizzle-demo

בואו נראה מה יש בפנים.

המשך קריאה

דוגמת קוד: שילוב Redux עם Next.js

10/01/2025

שילוב בין next.js ל Redux יכול להיות מבלבל כי כל אחת משתי הספריות רוצה לעשות משהו שהספריה השנייה עושה - בדוגמאות של next הם שמחים להראות איך עובדים רק עם next ומנהלים סטייט לגמרי בתוך העולם שלהם, ובדוגמאות של רידאקס הם שמחים להראות איך לכתוב store לעמוד אחד אבל כשצריך להתמודד עם מספר עמודים וסטייט בצד שרת יותר קשה למצוא דוגמאות.

בפוסט זה אני מציג רעיון אחד לשילוב בין הספריות שמבוסס על העקרונות הבאים:

  1. קוד צד שרת מכין אוביקט מידע (דרך שליפה מבסיס הנתונים, גישה לשירותי צד שלישי או כל מנגנון אחר).

  2. אוביקט המידע הזה הופך ל Store של רידאקס ונשלח לצד הלקוח.

  3. קומפוננטות שקוראות מרידאקס צריכות לבחור מה העמוד הראשי שהן מצפות להיות בו, כדי שנדע איזה State Object הן צפויות לקבל.

קוד ה Server Side Rendering יכול לעבוד עם אוביקט המידע שהשרת הכין וקוד צד לקוח יכול לעשות dispatch ל Actions לתוך אוביקט המידע הזה.

הריפו של הדוגמה זמין כאן:

https://github.com/ynonp/next-pages-redux

בואו נראה איך זה עובד.

המשך קריאה

תרגיל טייפסקריפט: פעולת עדכון גנרית ב Redux

07/01/2025

בדוגמת ההתחלה המהירה של 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 גנרית (אחת) שתקבל מפתח וערך מהסוג שמתאים לו ותכתוב את זה לאוביקט המידע.

המשך קריאה

לא מחכה לריאקט 19

04/10/2024

ריאקט התחילה בתור ספרייה לשכבת הממשק בלבד (UI Library). בשנים הראשונות הצוות שעבד על ריאקט חיפש את הדרך הטובה ביותר לבנות ממשק שיאפשר למפתחים לכתוב קומפוננטות לשימוש חוזר. הם עברו שתי מהפכות גדולות - מאובייקטים לקלאסים ומקלאסים לפונקציות. אחרי המעבר לפונקציות החל שינוי כיוון בפיתוח ריאקט והצוות התחיל לחפש "תבניות" שאנשים כותבים בריאקט ולהכניס אותן לתוך הפריימוורק. תקופה ארוכה התעסקנו עם Concurrent Mode ועם היכולת לאפשר לקומפוננטות לעצור באמצע תהליך Rendering, ואחרי שזה בוצע היעד הבא שלהם היא לפתור את ה Data Fetching וזה הסיפור של ריאקט 19.

גירסה 19 שכרגע יש לנו אותה ב RC בלבד מספקת מספר Hooks חדשים שהמטרה שלהם היא חיבור חזק יותר בין ריאקט לבין מנגנון העברת המידע בין הלקוח לשרת אצלנו ביישום:

  1. הפונקציה startTransition שחוזרת מ useTransition קיבלה תמיכה בקוד אסינכרוני, בשביל שיהיה אפשר לשלב אותה עם קריאות מהשרת.

  2. הוק חדש בשם useOptimistic נועד לאפשר שינוי מידע מקומי בצורה אופטימיסטית, כלומר לשנות את המידע בצורה זמנית, לפנות לשרת ואז רק אם הבקשה נכשלת לשנות אותו בחזרה.

  3. הוקים חדשים בשם useActionState ו useFormStatus נועדו להקל על הגשה של טפסים בצורה אסינכרונית.

  4. פונקציה חדשה בשם use (סוג של הוק) מאפשרת להשתמש ב Suspense לצורך Data Fetching ולבצע קריאה אסינכרונית מתוך קומפוננטה, בעצם סוג של לכתוב קומפוננטה אסינכרונית.

אין ספק שיש הרבה שיפורים מעבר לתמיכה המובנית ב Data Fetching - התמיכה ב Stylesheets מתוך קומפוננטה, התמיכה במאפיינים לכותרות המסמך ותיקוני API שקשורים ל ref ול useDeferredValue, ועדיין אני בטוח שהשינוי הבולט ביותר יהיה הפונקציה use. אז איפה הבעיה? כמו תמיד כשמפתחים API חדש יהיו בעיות, יהיו מקרי קצה מבלבלים, רוב האנשים ישתמשו בזה לא נכון ועוד שנתיים נגלה שחצי מהקוד שכתבנו לא מספיק יעיל או לא מטפל נכון באותם מקרי קצה. וכן זה בסדר לנסות לחדש, אבל הייתי שמח לראות את החידושים שקשורים ל Data Fetching תחומים לתוך ספריות כמו react query ולא גורמים לנו לארגן מחדש את הקוד וליצור "עוד דרך" לכתוב קומפוננטות ריאקט.

למה קשה ללמוד ריאקט נייטיב

30/07/2022

ריאקט נייטיב היא ספריית פיתוח מובייל מבוססת ריאקט, שהרעיון המרכזי שלה הוא שאתם כותבים קוד ריאקט תוך שימוש בקומפוננטות מיוחדות שלהם, והספריה ממפה בין הקומפוננטות שכתבתם לקומפוננטות טבעיות של מערכת ההפעלה בטלפון. במייקרוסופט מאוד התלהבו מהספריה והוסיפו מימושים גם ל Windows ו Mac כך שהיום אפשר לבנות בריאקט נייטיב גם אפליקציות טבעיות למערכות הפעלה אלה. לאלה שלא מכירים, ככה נראה דף בקוד ריאקט נייטיב:

// from: https://raw.githubusercontent.com/flatlogic/react-native-starter/master/src/modules/pages/PagesView.js

import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, Image } from 'react-native';

import { colors, fonts } from '../../styles';

const chartIcon = require('../../../assets/images/pages/chart.png');
const calendarIcon = require('../../../assets/images/pages/calendar.png');
const chatIcon = require('../../../assets/images/pages/chat.png');
const galleryIcon = require('../../../assets/images/pages/gallery.png');
const profileIcon = require('../../../assets/images/pages/profile.png');
const loginIcon = require('../../../assets/images/pages/login.png');
const blogIcon = require('../../../assets/images/pages/blog.png');

export default function PagesScreen(props) {
  return (
    <View style={styles.container}>
      <View style={styles.row}>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Charts')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={chartIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Charts</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Gallery')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={galleryIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Gallery</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Profile')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={profileIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Profile</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.row}>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Chat')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={chatIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Chats</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Calendar')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={calendarIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Calendar</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Auth')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={loginIcon}
            tintColor={colors.primary}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Login</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.row}>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Blog')}
          style={styles.blogItem}
        >
          <Image
            resizeMode="contain"
            source={blogIcon}
            tintColor={colors.primary}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Blog</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: colors.white,
    paddingTop: 10,
  },
  row: {
    flexDirection: 'row',
    paddingHorizontal: 10,
    marginTop: 10,
  },
  item: {
    flex: 1,
    height: 120,
    paddingVertical: 20,
    borderColor: colors.primaryLight,
    borderWidth: 1,
    borderRadius: 5,
    alignItems: 'center',
    justifyContent: 'space-around',
    marginHorizontal: 5,
  },
  blogItem: {
    width: '31%',
    height: 120,
    paddingVertical: 20,
    borderColor: colors.primaryLight,
    borderWidth: 1,
    borderRadius: 5,
    alignItems: 'center',
    justifyContent: 'space-around',
    marginHorizontal: 5,
  },
  itemText: {
    color: colors.primary,
    fontFamily: fonts.primary,
  },
  itemImage: {
    height: 35,
  },
});

למרות שהטכנולוגיה בגדול מופלאה ועובדת, ללמוד ריאקט נייטיב יכול להיות משימה לא פשוטה ממספר סיבות:

  1. בגלל שמכונות היעד הן טלפונים, כדאי שיהיו לנו כמה טלפונים לבדיקה והיכרות לפחות בסיסית עם המושגים בפיתוח מובייל ובנוסף עם סביבות הפיתוח XCode ו Android Studio. זה לבד הרבה חומר ללמוד.

  2. פיתוח על טלפונים יכול להיות מאתגר כי כל טלפון הוא שונה, ובמהלך הפיתוח אני יכול לבנות קוד שיעבוד יופי על מכשיר או כמה מכשירים, אבל יישבר או יראה רע על מכשירים אחרים. הצרות של פיתוח מובייל לא נעלמות רק בגלל שריאקט נייטיב מרנדר את הממשק.

  3. ריאקט נייטיב עצמו הוא די רזה וכמעט בכל אפליקציה נרצה להוסיף ספריות הרחבה. קוד הדוגמה שהודבק למעלה למשל משתמש בספריית React Navigation כדי לנווט בין דפים באפליקציה. יש ספריה חיצונית שמטפלת בשמירת מידע על הדיסק ועוד ספריות רבות לנושאים לכאורה בסיסיים בפיתוח.

  4. עיצוב ריאקט נייטיב, למרות שנראה במבט ראשון כמו CSS (או יותר נכון כמו CSS In JS), עדיין דורש למידה ובדיקה גם לאנשים שמכירים טוב CSS כי לא כל המאפיינים נתמכים או נתמכים בכל הקומפוננטות.

ועדיין אם אתם שוקלים פיתוח של אפליקציית מובייל, שווה להתאמץ ולהשקיע זמן בללמוד ריאקט נייטיב במיוחד אם האפליקציה פשוטה ואתם כבר מכירים ריאקט. העבודה איתה יותר פשוטה ומביאה תוצאות מהר יותר בהשוואה לפיתוח נייטיב רגיל, במיוחד אם צריך לבנות את האפליקציה לכמה מערכות הפעלה.

אפקטים וקוד איתחול בריאקט 18

24/07/2022

הרבה זמן המהנדסים שעובדים על ריאקט עשו מאמץ לא לשבור כלום. כשהיה צריך דרך חדשה לעשות דברים - למשל כשנכנסו Hooks לתמונה - זה תמיד היה בנוסף לדרך הקיימת ולא במקום. אפילו את תחביר המיקסינים הישן אפשר היה להפעיל בעזרת ספריה חיצונית שנים אחרי שהתמיכה המובנית בו בוטלה.

בגירסה 18 נראה שהמאמץ הזה הסתיים.

מנגנון Concurrent Mode שובר את הרעיון ש render של כל הקומפוננטות מתבצע "במכה אחת", ולכן פותח דלת לבאגים של סינכרון בין render-ים של קומפוננטות שונות. יכולים לקרוא על הבעיה כאן.

מנגנון נוסף שנשבר הוא useEffect, או יותר ספציפית השימוש ב useEffect כדי לבצע פעולה חד-כיוונית כשמשהו קורה. הנה דוגמה מתוך התיעוד של expo, ספריית פיתוח שעוטפת את react native:

import React, { useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import * as Brightness from 'expo-brightness';

export default function App() {
  useEffect(() => {
    (async () => {
      const { status } = await Brightness.requestPermissionsAsync();
      if (status === 'granted') {
        Brightness.setSystemBrightnessAsync(1);
      }
    })();
  }, []);

  return (
    <View style={styles.container}>
      <Text>Brightness Module Example</Text>
    </View>
  );
}

הקוד המקורי בקישור: https://docs.expo.dev/versions/v45.0.0/sdk/brightness/#brightnessrequestpermissionsasync

המטרה של האפקט היא לבקש מהמשתמש גישה לשינוי רמת הבהירות של התצוגה - פעם אחת ורק כשהאפליקציה עולה. יש פה הנחה שהקומפוננטה הראשית תעשה mount פעם אחת בלבד כשהאפליקציה עולה ולעולם לא תעשה unmount.

וזאת בדיוק ההנחה שנשברת בריאקט 18: הנחת העבודה של ריאקט 18 (והלאה) היא שקומפוננטות יכולות תמיד לצאת מהמסך ולהיכנס חזרה. ריאקט שומר לעצמו את הזכות להוציא ולהכניס קומפוננטות למסך כשזה יתאים לו, ומצב הפיתוח של ריאקט 18 מבהיר את זה כשב Strict Mode אוטומטית כל קומפוננטה עוברת unmount ואז שוב mount כשהיא נכנסת למסך.

במבט קדימה בעבודה עם ריאקט וכדי לא לשבור דברים נרצה לכוון לעבוד עם אפקטים אך ורק כדי לסנכרן בין קומפוננטה למשאב חיצוני - כך שקוד האפקט מפעיל איזושהי פעולות subscribe ומחזיר פונקציה שעושה unsubscribe; את קוד האיתחול של המערכת שבאמת צריך לקרות רק פעם אחת, לדוגמה קוד שמבקש הרשאות, נצטרך להתחיל להריץ מחוץ לקומפוננטות בקובץ איתחול מסודר.

חדש באתר: עדכון תכני Redux ו React Router בקורס ריאקט

18/07/2022

העולם זז קדימה ואיתו גם אנחנו, והיום העדכון הוא בקורס ריאקט.

את הקורס הקלטתי ב 2020 כש Hooks עוד היו דבר חדש, וכשעוד קיוויתי ש create-react-app לא באמת יתפוס.

מאז create-react-app תפס הרבה יותר מדי חזק, והיום אי אפשר לזוז בלעדיו. הוקס השתלטו על העולם והם הדרך הסטנדרטית לבנות ממשקים בריאקט, וריאקט ראוטר כרגיל עשה עוד re-write שלא תומך אחורה.

מה שמביא אותנו לעדכון האחרון-

  1. נוסף שיעור על create-react-app.

  2. רוב פרק Redux הוקלט מחדש כי היום כולם משתמשים ב Redux Toolkit, והוא גם הרבה יותר טוב משימוש ברידאקס "נקי". פרסמתי בבלוג לא מזמן פוסט היכרות איתו, ואם אתם מנויים שווה פשוט להיכנס לקורס ולראות את השיעורים הרלוונטים.

  3. פרק React Router הוקלט מחדש כדי להתאים ל React Router 6. אני רק מקווה שהחבר'ה שם יתעייפו לפניי מכל ה Breaking Changes שלהם.

אם מאיזושהי סיבה אתם צריכים גישה לסרטים הישנים שלחו לי מייל ואנסה לראות איך לשלוח לכם.

געגועים ל Higher Order Components

08/07/2022

לפני Hooks היינו משתמשים בריאקט בתבנית קצת מסורבלת שנקראה Higher Order Components כדי לשתף Stateful Logic בין כמה קומפוננטות, מה שהיום אנחנו עושים בקלות עם Custom Hooks. אבל הרעיון מאחורי Higher Order Components יכול לעזור גם בכתיבת קומפוננטות מודרניות ולאפשר שיתוף קוד טוב ופשוט באפליקציה. ככה זה עובד.

המשך קריאה

מבוא ל React Router גירסה 6

04/07/2022

בעולם הישן של פיתוח ווב, לכל דף באתר היתה כתובת (URL) משלו, ודף HTML שהשרת שולח כשגולש נכנס לאותה כתובת. בריאקט, ובמיוחד אם יצרתם את האפליקציה עם create-react-app, זה קצת יותר קשה ליישום. אם היינו רוצים ללכת בדרך זו, זה היה אומר שצריך אפליקציית ריאקט חדשה עבור כל דף בגלל ש create-react-app מייצר רק קובץ HTML אחד. אבל אפילו אם נצליח לייצר כמה קבצי HTML, הניהול של כל העסק הזה לא שווה את המאמץ.

במקום זה הדרך המקובלת לעבור בין דפים נקראת Single Page Application. הרעיון שיש לנו רק קובץ HTML אחד עם סט אחד של קומפוננטות ריאקט, וקוד ריאקט יודע להציג את הקומפוננטה שמתאימה ל URL הנוכחי. ספריית react-router, עליה נלמד בפרק זה, מספקת דרך קלה לבניית יישומים כאלה בריאקט.

המשך קריאה

ריאקט 18 סוף סוף מציע פיתרון לבעיית ה label-ים

02/07/2022

עד שהגיע ריאקט או בכלל הרעיון של קומפוננטות, מתכנתים ומתכנתות כתבו קבצי HTML גדולים ובתוכם היו label-ים ו input-ים המתואמים ביניהם באמצעות id:

<label for="name">User Name</label>
<input type="text" id="name" />

ואז בא ריאקט ולימד אותנו שלא כדאי להשתמש ב id על אלמנטים כי אולי מישהו הולך להשתמש בקומפוננטה שלך בעוד כמה מקומות ואז יהיה במסמך id כפול. מפה מתכנתים ומתכנתות התחילו לחפש פיתרונות יצירתיים, החל מ:

<label>
User Name
<input type="text" />
</label>

ועד דברים יותר מתוחכמים כמו המצאת מזהים:

function MyForm() {
    const id = Math.random().toString(16);
    return (
        <>
            <label htmlFor={id}>User Name</label>
            <input type="text" id={id} />
        </>
    );
}

הבעיה בפיתרון הראשון היא שהוא מכריח markup מסוים. כל עוד זה עובד לכם הכל טוב, אבל לפעמים באמת ה label וה input לא צמודים אחד לשני. הבעיה בפיתרון השני היא שכנראה לא נקבל את אותו id ב Server Side Rendering, מה שייצור אי תאימות כשריאקט יריץ את כל ה render-ים בדפדפן ויקבל HTML עם מזהים שונים.

ריאקט 18 סוף סוף מציעים פיתרון קל ונוח לבעיה עם הוק בשם useId. פונקציה זו תחזיר מזהה ייחודי ותבטיח לכם שתקבלו את אותו מזהה ברינדור בצד שרת ובצד הלקוח. לכן הדרך הנכונה בריאקט 18 לכתוב את אותו קוד היא:

import { useId } from 'react';

function MyForm() {
    const id = useId();
    return (
        <>
            <label htmlFor={id}>User Name</label>
            <input type="text" id={id} />
        </>
    );
}

ה id שמקבלים לא יצירתי במיוחד ובדוגמה שניסיתי קיבלתי מחרוזת כמו :r1:, :r3 ו :r5:. אני לא יודע למה דווקא r ולמה הוא מדלג על הזוגיים. אם יש לכם רעיון או מידע פנימי אשמח לשמוע בתגובות.