• בלוג
  • קומפוננטות אסינכרוניות בריאקט 19

קומפוננטות אסינכרוניות בריאקט 19

08/12/2024

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

1. איך ליצור תוכנית ריאקט 19 עם Vite

בגלל שהכל חדש תוכנית ריאקט חדשה שניצור ב vite עדיין עובדת עם ריאקט 18. כדי לשדרג לריאקט 19 יש לשנות את גירסאות הספריות מ 18 ל 19 בקובץ package.json. זאת הרשימה אצלי בפרויקט חדש ששודרג לריאקט 19:

{
  "name": "react-19-async-components",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": ">=19",
    "react-dom": ">=19"
  },
  "devDependencies": {
    "@eslint/js": "^9.15.0",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vitejs/plugin-react": "^4.3.4",
    "eslint": "^9.15.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "globals": "^15.12.0",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.15.0",
    "vite": "^6.0.1"
  }
}

2. יצירת קומפוננטה אסינכרונית

הגדרת קומפוננטה אסינכרונית בריאקט לצערנו לא קורית עם פקודת async כמו הגדרה של פונקציה אסינכרונית רגילה אלא באמצעות שימוש ב Hook חדש שנקרא use. הפונקציה use יודעת לקבל Promise, והקומפוננטה "תופעל" רק כשה Promise מתממש. נשים לב שלפני שנוכל להפעיל את הקומפוננטה האסינכרונית עלינו לעטוף את העץ שיוצר אותה באלמנט Suspense של ריאקט, כלומר הקוד הראשי ב App.tsx הוא:

import { Suspense } from 'react'
import AsyncData from './AsyncData';
import './App.css'

function App() {
  return (
    <main>
    <h1>Async Component Demo</h1>
    <Suspense fallback={<p>Loading, please wait ...</p>}>
      <AsyncData />
    </Suspense>
  </main>
  )
}

export default App

בצד של הקומפוננטה עלינו ליצור Promise מחוץ לקומפוננטה, להעביר אותו פנימה לתוך פקודת use ולהשתמש במידע שחוזר ממנו:

import {use} from 'react';

const dataPromise = fetch(`https://swapi.dev/api/people/1`).then(r => r.json());

export default function() {
  const data = use(dataPromise);
  return (
    <div>
      <p>Ready!</p>
      <p>Received Data for: {data.name}</p>
    </div>
  )
}

שימו לב שה Promise נוצר מחוץ לפונקציית הקומפוננטה. יצירת Promise היא Side Effect ולכן אם נקרא ל fetch בתוך פונקציית הקומפוננטה כל פעם שריאקט יצטרך לרנדר את הקומפוננטה מחדש הוא יפעיל את ה fetch מחדש וניכנס ללולאה אינסופית של משיכת מידע ואז קריאה לפונקציית הקומפוננטה כדי לקבל את ה JSX, מה שיגרום להפעלה חדשה של ה fetch ומשיכת המידע מחדש וכן הלאה. אם אתם צריכים למשוך מידע כפונקציה של הקומפוננטה (למשל לפי id שעובר ב props) התבנית הכי טובה שמצאתי בינתיים היא לפצל את הקומפוננטה ל-2 ולשים בלוק Suspense נוסף בקומפוננטה העוטפת, כלומר קוד כזה:

import {Suspense, use, useState, useEffect} from 'react';

export default function(props: {id: number}) {
  const {id} = props;
  const [activeFetch, setActiveFetch] = useState(Promise.resolve({} as Record<string, string>));

  useEffect(() => {
    setActiveFetch(fetch(`https://swapi.dev/api/people/${id}`).then(r => r.json()))
  }, [id])

  return <Suspense>
      <ShowAsyncData dataPromise={activeFetch} />
    </Suspense>
}

function ShowAsyncData(props: {dataPromise: Promise<Record<string, string>>}) {
  const {dataPromise} = props;
  const data = use(dataPromise);
  return (
    <div>
      <p>Ready!</p>
      <p>Received Data for: {data.name}</p>
    </div>
  )
}

הסיבה ש useEffect לבד לא מספיק היא שה Suspense מחליף את הקומפוננטה שבתוכו בתוצאה של הקומפוננטה האסינכרונית, החלפה ששקולה לשינוי key, כלומר ממש יוצרים מחדש את הקומפוננטה - ולכן האפקט יופעל מחדש, אפילו ש id לא השתנה. לכן חייבים ליצור את ה Promise מחוץ לבלוק ה Suspense שיושפע מה use.

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

https://www.tocode.co.il/blog/2024-10-vue-async-fetch

דעתי האישית שהמנגנון של vue הרבה יותר פשוט והרבה פחות מבלבל.