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

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

הבעיה של ריאקט ו State

13/06/2025

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

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

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

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

ניקח דוגמה פשוטה של מונה לחיצות:

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="counter" >
      <p>Count = {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}

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

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import Counter from './Counter'
import CounterGroup from './CounterGroup'
function App() {

  return (
    <CounterStatistics />
    <Counter />
    <Counter />
    <Counter />
  )
}

export default App

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

function App() {
  const [counterValues, inc] = useCounters(3);
  return (
    <>
      <CountersStats values={counterValues} />      
      <Counter value={counterValues[0]} inc={inc(0)} />
      <Counter value={counterValues[1]} inc={inc(1)} />
      <Counter value={counterValues[2]} inc={inc(2)} />
    </>
  )
}

ועכשיו הייתי צריך לשנות את הקומפוננטה של Counter שלא תכיל סטייט פנימי רק בשביל לשים אותה בתוך קומפוננטה שתציג סטטיסטיקות.

וכן אני מבין מה קורה פה ולמה היינו צריכים ללכת את הדרך הזאת, אבל באמת יש פה שאלה אם זה שווה את המאמץ, ואולי דווקא במקרים כאלה כן שווה לשכפל את הסטייט ולשמור סטייט נפרד נוסף לסטטיסטיקות, כלומר:

function App() {
  const [min, setMin] = useState(0);
  const [max, setMax] = useState(0);

  function update(value) {
    setMin(Math.min(min, value));
    setMax(Math.max(max, value));
  }

  return (
    <>
      <CountersStats />
      <Counter onChange={update} />
      <Counter onChange={update} />
      <Counter onChange={update} />
    </>
  )
}

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

מה דעתכם?

אופס שברתי את next

14/05/2025

את הקוד הבא כתבתי בטעות ואני מודה שהתוצאה הפתיעה אותי:

const data = await fetch('https://api.vercel.app/blog', {
    cache: 'no-store',
    next: {
        revalidate: 60
    }
})

הקוד עבד בפיתוח אבל גרם ל build להיתקע. בואו נבין מה קורה פה.

המשך קריאה

תבנית פרויקט: 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 יכול לעזור גם בכתיבת קומפוננטות מודרניות ולאפשר שיתוף קוד טוב ופשוט באפליקציה. ככה זה עובד.

המשך קריאה