• בלוג
  • עמוד 85
  • מדריך: יצירת אפליקציית React בתוך שרת Rails 7 - חלק 2 - העברת משתנים

מדריך: יצירת אפליקציית React בתוך שרת Rails 7 - חלק 2 - העברת משתנים

29/05/2022

פוסט זה הוא חלק שני של מדריך הקמת יישום React ו Rails ב Rails 7. אם פספסתם החלק הראשון זמין בקישור:

https://www.tocode.co.il/blog/2022-05-react-rails-7-vite-rails

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

1. הבעיה עם משתנים

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

לריילס יש מנגנון די טוב להעברת מידע מ Controller ל View שמתאים לו, ואלה הם משתני ה @ של הקונטרולר. כלומר אני יכול לכתוב בקוד של קונטרולר:

def index
    @page_name = "Homepage"
end

ואחרי זה ב View אני יכול פשוט להשתמש ב @page_name בתוך ה HTML של העמוד:

<h1><%= @page_name %></h1>

הבעיה שאנחנו כבר לא בקנזס - או יותר נכון לא משתמשים ב Views של Rails. מה שיותר גרוע הוא שכל מנגנון מעברי הדפים שלנו משתמש ב React Router ולא ב Turbolinks, ולכן אפילו אם היינו מצליחים לגרום לדף הראשון להציג מידע דרך ה Views (למשל עם ג'ם כמו Gon), אחרי מעבר עמוד לא היה שום דבר שגורם לעמוד החדש להציג את המידע המעודכן.

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

2. פיתרון באמצעות REST API

הפיתרון שנבנה יתבסס על ג'ם שנקרא responders. ג'ם זה מאפשר להגדיר שכל קונטרולר יוכל להגיב לכל מיני סוגים של בקשות - במקרה שלנו נגדיר שאם מבקשים מקונטרולר דף HTML הוא ישלח דף רגיל כמו שעשה עד עכשיו, וישתול את המשתנים בתוך אלמנט נסתר בדף הזה. אבל, אם מבקשים מאותו קונטרולר לקבל JSON, אז הוא ישלח רק את המשתנים בתור JSON.

תחילה אני מתקין את הג'ם עם:

$ bundle add responders

עכשיו אני הולך ל application_controller.rb ומגדיר לו שהוא וכל קונטרולר שיורש ממנו יסכים לענות לבקשות ל html וגם לבקשות ל json:

class ApplicationController < ActionController::Base
  respond_to :html, :json
end

נמשיך ל home_controller.rb ונגדיר שם משתנה, אני אקרא לו @page, שיכיל אוביקט עם מפתח בשם page שהתוכן שלו הוא כותרת העמוד:

class HomeController < ApplicationController
  def index
    @page = { page: 'Home Page' }
    respond_with(@page)
  end
end

אותו דבר אני כותב ב about_controller.rb רק עם כותרת עמוד שונה:

class AboutController < ApplicationController
  def index
    @page = { page: 'About Page' }
    respond_with(@page)
  end
end

הפקודה respond_with מגיעה מהג'ם responder והיא המקור לרוב הקסם כאן. היא מתאימה לפקודת ה respond_to שכתבנו בראש הקונטרולר הראשי.

באופן אוטומטי respond_with יודעת לקחת את המשתנה שנתנו לה ולהפוך אותו ל JSON אם מישהו מבקש JSON. בשביל לשתול את המשתנה הזה ב HTML אנחנו צריכים לכתוב קצת קוד. נלך ל app/views/layout/application.html.erb ושם מתחת ל body אני מוסיף את הבלוק הבא:

<% if defined?(@page) %>
    <script type="text/json" id="page-data" data-url="<%= request.fullpath %>">
    <%= raw @page.to_json %>
    </script>
<% end %>

עכשיו בכל טעינת עמוד ריילס ישתול בתוכו אלמנט script עם תוכן המשתנה @page, ובכל מעבר עמוד ריילס ישלח את תוכן המשתנה @page בתור אוביקט JSON ללקוח.

3. תיקון צד לקוח

בצד של ריאקט אני צריך למשוך את המידע ולהעביר אותו לדפים. דרך קלה לעשות את זה היא לרנדר אלמנט Layout כללי לכל הדפים שיהיה אחראי על משיכת המידע מהשרת או מהתוכן שכבר מוטמע ב HTML, ויעביר אותו פנימה לאלמנט שמתאים לדף הנוכחי. בשביל להוסיף אלמנט Layout מסביב לאלמנט דף הבית או דף האודות אני משתמש בקוד הבא בקובץ entrypoints/application.jsx:


function App() {
  return (
    <Router>
        <Routes>
          <Route element={<Layout />}>
            <Route path="/" element={<Homepage />} />
            <Route path="/about/index" element={<About />} />
          </Route>
        </Routes>
    </Router>
  );
}

אלמנט Layout הוא בסך הכל קומפוננטת ריאקט שמושכת את המידע ומעבירה אותו ל Homepage או About בהתאמה. רק בשביל ההבנה הוספתי כותרת משותפת לכל הדפים והדפסה של הנתיב שאנחנו כרגע מבקרים בו. זה הקוד של Layout:

function Layout() {
  const location = useLocation();
  const url = useHref(location);
  const { data, error } = useSWR(url, fetcher)

  if (error) {
    console.log(error);
    return <p>Error: {JSON.stringify(error)}</p>
  }

  return (
    <div>
      <h1>My App</h1>
      <p>Visiting: {url}</p>
      {data && <Outlet context={data} />}
    </div>
  );
}

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

const fetcher = async (url) => {
  const pageData = document.querySelector(`#page-data[data-url="${url}"]`);
  if (pageData) {
    console.log(`Reading data from page`);
    return JSON.parse(pageData.innerHTML);
  } else {
    const res = await fetch(url, { headers: { accept: 'application/json' } })
    return await res.json();
  }
}

עכשיו אני יכול להמשיך לדפים עצמם ושם למשוך את המידע ולהשתמש בו. המשיכה משתמשת בפונקציה של React Router שנקראת useOutletContext והקוד (לדוגמה של דף הבית) נראה כך:

import React, { useState } from 'react';
import { Link, useOutletContext } from "react-router-dom";

export default function Homepage() {
  const [name, setName] = useState('');
  const { page } = useOutletContext();

  return (
    <div>
      <h2>Page Name From Rails: {page}</h2>
      <label>
        Please type your name:
        <input type="text" value={name} onChange={(e) => setName(e.currentTarget.value)} />
      </label>
      {name !== '' && <p>Hello! {name}</p>}
      <Link to="/about/index">About Us</Link> 

    </div>
  );
}

4. סיכום

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

class HomeController < ApplicationController
  def index
    @page = { page: 'Home Page' }
    respond_with(@page)
  end
end

כשאולי הדבר היחיד שמוזר כאן זה שהשם חייב להיות @page כדי להתאים לקוד שכתבנו ב layout.

בצד הלקוח אני משתמש בפונקציה useOutletContext כדי למשוך את המשתנה page, שיכול להיות גם אוביקט JSON מורכב שמכיל תוצאה של שאילתה או שאילתות מסובכות, ומשתמש בו בקומפוננטה:

import React, { useState } from 'react';
import { Link, useOutletContext } from "react-router-dom";

export default function Homepage() {
  const [name, setName] = useState('');
  const { page } = useOutletContext();

  return (
    <div>
      <h2>Page Name From Rails: {page}</h2>
      <label>
        Please type your name:
        <input type="text" value={name} onChange={(e) => setName(e.currentTarget.value)} />
      </label>
      {name !== '' && <p>Hello! {name}</p>}
      <Link to="/about/index">About Us</Link> 

    </div>
  );
}

את הקוד המלא תוכלו למצוא בריפו: https://github.com/ynonp/react-rails-vite-demo