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

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

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

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

טיפ ריאקט: איך לא להשתמש ב useDeferred

29/06/2022

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

נתבונן בקוד הבא:

function BrokenHugeList() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }
  console.log(`1 seach = ${deferredSearch}`);
  const searchResults = items.filter(i => i.includes(deferredSearch));
  console.log('2');

  return (
    <div>
      <input type="text" value={search} onChange={handleChange} />
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
}

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

וזה באמת מה שקורה רק שצריך לזכור שתי נקודות שבסוף יכולות לפעול לרעתנו:

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

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

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

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

const HugeList = React.memo(function HugeList(props) {
  const { search } = props;
  console.log(`3, search = ${search}`);
  const searchResults = items.filter(i => i.includes(search));
  console.log('4');

  return (
    <div>
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
});

function App() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }

  return (
    <>
      <input type="text" value={search} onChange={handleChange} /> 
      <HugeList search={deferredSearch} />
      <hr />
    </>
  );
}

נ.ב. גם אחרי השינוי, העבודה עם 50 אלף פריטים אומרת שמהירות התגובה של הקומפוננטה לא תהיה מזהירה, אבל עם כמה אלפים או אפילו 20 אלף פריטים שימוש ב useDeferredValue כן יכול לתת שיפור מורגש בזמני התגובה.

היכרות עם Redux Toolkit

27/06/2022

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

המשך קריאה

אני לא מאמין ששוב טעיתי ב useEffect

31/05/2022

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

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

import React, { useEffect } from 'react';

// trackedRef is a reference to a DOM element
export default function ResizeHandler({ trackedRef }) {
  useEffect(() => {
    const resizeObserver = new ResizeObserver(() => {
      const height = trackedRef.current.scrollHeight;
      console.log(`Your new height is: ${height}`);
    });
    resizeObserver.observe(trackedRef.current);

    return function cancel() {
      resizeObserver.disconnect();
    }
  }, [trackedRef]);

  return <></>;
}

רואים את הבאג? מוזמנים לשתף בתגובות ולהיזהר מסכינים חדות.

(נ.ב. מכל הפונקציות של ריאקט, useEffect היתה בשבילי אהבה ממבט ראשון. הגעתי אליה עם הטראומה של הכפילות בין componentDidMount ו componentDidUpdate וחשבתי שהנה יש פיתרון לכל הצרות שלי במקום אחד. בפועל חוסר תשומת לב כמו בדוגמה כאן גורם לי לייצר אינסוף באגים יצירתיים בעבודה איתה. במקרה הזה למרות שהשתמשתי ב eslint ולמרות ש eslint כולל כלל ספציפי להתמודדות עם הבאג בקוד המודבק הוא עדיין התעלם מהטעות ולא הזהיר. אין לי פה המלצות קסם חוץ מלהיות מאוד זהירים בעבודה עם useEffect, לכתוב אפקטים קצרים ולא יותר מדי מהם ולקוות שבאיטרציה הבאה של ריאקט דן אברמוב יבוא עם מנגנון יותר ידידותי).

היום למדתי: מה באמת הפריע כל כך ב class ב React

27/05/2022

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

Warning: Invalid DOM property `class`. Did you mean `className`?

אבל כבר כמה שנים למרות האזהרה הקוד עדיין עובד. כלומר בקומפוננטה הזו:

import "./styles.css";

export default function App() {
  return (
    <div class="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

ל div המרכזי יהיה את הקלאס App.

אז מה בכל זאת הבעיה? מסתבר שהסיפור הוא בכלל לא מגבלה טכנית של ריאקט אלא ניסיון להגן עלינו המתכנתים. בשביל לכתוב קומפוננטה שמקבלת בתור prop מאפיין class יכול להיות שהיינו מנסים לכתוב קוד כזה:

export default function App(props) {
    const { class } = props;
}

שלא היה עובד בגלל ש class זו מילה שמורה.

וכן ברור שהיינו יכולים לכתוב במקום משהו כזה:

export default function App(props) {
    const { class: className } = props;
    // now I use className inside the component, to handle the "class" from outside
}

אבל אף אחד לא רוצה לשנות עכשיו את כל הקוד של קומפוננטות קיימות.

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