מימוש יישום מרובה שפות באמצעות React Context

21/03/2019

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

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

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

  1. מדובר על מידע גלובאלי שכל רכיב ביישום משתמש בו.

  2. מדובר על מידע שביסודו הוא Immutable, או לפחות שלא משתנה לעתים קרובות.

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

דוגמאות נפוצות ל Context כוללות: הגדרות שפה, הגדרות עיצוב גלובאליות ו Redux Store.

1. נעבור לקוד היישום

ניקח לדוגמא הגדרות שפה. היישום שלנו יהיה בנוי בצורה הבאה:

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

  2. נבחר את השפה הפעילה ונשמור את כל המחרוזות של השפה הנוכחית ב Context.

  3. כל פקד שצריך להציג מחרוזות יוכל לשלוף מה Context את המחרוזת המתאימה לפי השפה הנוכחית.

  4. כשנחליף שפה, כל הפקדים ירונדרו מחדש עם המחרוזות מהשפה החדשה.

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

2. קובץ השפות

במקרה של הגדרות שפה אני חושב שכדאי לשמור ב Context את פונקציית התרגום. זוהי פונקציה שמקבלת מזהה מחרוזת ומחזירה את הטקסט המתאים בשפה הנוכחית. כשרוצים לשנות שפה ביישום נחליף את הפונקציה הזו בפונקציית תרגום לשפה אחרת, ואז כל הקומפוננטות ירונדרו מחדש.

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

import { createContext } from "react";

export const i18n = {
  en: {
    hello: "hello",
    whats_your_name: "who are you?",
    byebye: "Bye Bye",
    nice_to_meet_you: "Nice to meet you",
    change_lang: "Change Language",
    current_lang: "English",
    current_lang_label: "Current Language: "
  },
  he: {
    hello: "שלום",
    whats_your_name: "איך קוראים לך?",
    byebye: "להתראות",
    nice_to_meet_you: "נעים להכיר",
    change_lang: "שנה שפה",
    current_lang: "עברית",
    current_lang_label: "השפה הנוכחית היא: "
  }
};

export default createContext(id => i18n.en[id]);

3. שימוש במשתנה מהקונטקסט דרך פקד שהוא פונקציה

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

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

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

import React, { useState, useContext } from "react";
import i18n from "./lang";

export default function Greeter(props) {
  const [name, setName] = useState("");
  const t = useContext(i18n);

  return (
    <>
      <label>
        {t("hello")}, {t("whats_your_name")}
        <input
          type="text"
          value={name}
          onChange={e => setName(e.target.value)}
        />
      </label>
      <p>
        {t("nice_to_meet_you")} {name}
      </p>
    </>
  );
}

4. שימוש בקונטקסט מתוך פקד מחלקה

בפקד מסוג מחלקה צריך להתאמץ קצת יותר כדי לקבל את פונקציית התרגום מה Context, אבל גם זה לא כל כך נורא. לכל אוביקט ש createContext מחזירה יש שדה בשם Consumer שהוא בעצם קומפוננטה. בשביל להשתמש ב Context אנחנו עוטפים את הקומפוננטה שלנו באותו Consumer. ה Consumer משתמש בתבנית של render props כדי לתת לנו את הערך שנשמר ב Context.

קוד מחלקה לדוגמא שמשתמשת בפונקציית התרגום ששמרתי ב Context נראה כך:

import React from "react";
import I18n from "./lang";

export default class Header extends React.Component {
  render() {
    return (
      <I18n.Consumer>
        {t => (
          <p>
            {t("current_lang_label")} {t("current_lang")}
          </p>
        )}
      </I18n.Consumer>
    );
  }
}

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

5. דריסת הערך מה Context כדי לאפשר שינוי שפה

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

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

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

לכן בשביל לאפשר לאנשים להחליף שפה נשתמש במבנה הבא:

  1. נשמור את השפה הנוכחית במשתנה ב State.

  2. נגדיר שתי פונקציות תרגום חדשות: אחת לתרגום לאנגלית והשניה לתרגום לעברית.

  3. נעביר את פונקציית התרגום שמתאימה לשפה הנוכחית בתור ה value של ה Provider.

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

const translationFunctions = {
  en: id => i18n.en[id],
  he: id => i18n.he[id]
};

function App() {
  const [lang, setLang] = useState("en");

  function toggleLang() {
    setLang(lang === "en" ? "he" : "en");
  }

  return (
    <I18n.Provider value={translationFunctions[lang]}>
      <div className="App">
        <Header />
        <Greeter />
        <LanguageSelector toggleLang={toggleLang} />
      </div>
    </I18n.Provider>
  );
}

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

הקוד המלא זמין בסנדבוקס הבא: