ניתוב צד-לקוח עם react router

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

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

1. הטוב: ניתוב צד-לקוח פשוט ליישומי ריאקט

הדבר הראשון שאפשר לאהוב בספריה זה כמה קל להתחיל לעבוד אתה ולהמיר יישום ריאקט ליישום צד-לקוח מלא. זה קוד שאני משתמש בו כסטארטר בפרויקטי react-router חדשים (את הפרטים המלאים על הספריה והתחביר למי שלא מכיר אני ממליץ לחפש בתיעוד):

החלק המעניין הוא כמובן טבלת הניתוב:

var Routes = React.createClass({
  render: function() {
    return (<Router history={hashHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={Index} />
      <Route path="about" component={About}/>
      <Route path="users" component={Users} />
      <Route path="hello/:name" component={Hello} />
    </Route>
  </Router>)
  }
});

2. הרע: ניהול לוגיקה של מעבר נתיב

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

הקובץ שאחראי על מעברי עמודים והרצת קוד שלכם בעת מעבר עמוד נקרא createTransitionManager ואפשר למצוא אותו בקוד של react-router. כך נראית הפונקציה finishMatch מתוכו, פונקציה שנקראת בכל פעם שזוהה מעבר נתיב ולאחר שזיהינו איזה רכיבים יש להחליף:

  function finishMatch(nextState, callback) {
    const { leaveRoutes, enterRoutes } = computeChangedRoutes(state, nextState)

    runLeaveHooks(leaveRoutes)

    // Tear down confirmation hooks for left routes
    leaveRoutes
      .filter(route => enterRoutes.indexOf(route) === -1)
      .forEach(removeListenBeforeHooksForRoute)

    runEnterHooks(enterRoutes, nextState, function (error, redirectInfo) {
      if (error) {
        callback(error)
      } else if (redirectInfo) {
        callback(null, createLocationFromRedirectInfo(redirectInfo))
      } else {
        // TODO: Fetch components after state is updated.
        getComponents(nextState, function (error, components) {
          if (error) {
            callback(error)
          } else {
            // TODO: Make match a pure function and have some other API
            // for "match and update state".
            callback(null, null, (
              state = { ...nextState, components })
            )
          }
        })
      }
    })
  }

הבעיה בארכיטקטורה הזו שאנחנו מאוד תלויים בקוד של react-router ובמבנה שלו: יש דברים שקל לעשות ודברים שקשה. קל מאוד להגדיר עבור נתיב מסוים Enter Hook שיקרא בכל כניסה לנתיב (למשל בשביל לבדוק אם הגולש כבר נרשם לאתר, ואם לא אז להפנות אותו לעמוד הרישום). הרבה יותר קשה לשתף את ה Enter Hooks האלה בין נתיבים שונים. הקוד הבא לדוגמא לא עובד:

var LogRoute = function(props) {
  return <Route {...props} onEnter={logRouteChange} />
}

var Routes = React.createClass({
  render: function() {
    return (<Router history={hashHistory} >
    <Route path="/" component={App}>
      <IndexRoute component={Index} />
      <LogRoute path="about" component={About} />
      <LogRoute path="users" component={Users} />
      <LogRoute path="hello/:name" component={Hello} />
    </Route>
  </Router>)
  }
});

אתגר נוסף הוא מחסור ב Hooks. נכון למועד כתיבת שורות אלו, Pull Request להוספת On Change Hook ממתין כבר מעל 10 ימים. הבעיה ששינוי זה יפתור היא ש Enter Hook נקרא רק בכניסה לנתיב, ואין לנו היום דרך לקבל עדכון כשפרמטרים של נתיב משתנים (רק טיפול מפורש בקוד הפקד).

בטבלת הניתוב הבאה הנתיב photo/:id מציג תמונה לפי מזהה. הפונקציה checkPermissions מוודאת שלמשתמש הנוכחי יש הרשאות לצפות בתמונה המבוקשת. הבעיה היא שהפונקציה תקרא בכניסה הראשונה לנתיב אך לא תקרא בדפדוף בין תמונות:

var Routes = React.createClass({
  render: function() {
    return (<Router history={hashHistory} >
    <Route path="/" component={App}>
        <Route path="photo/:id" onEnter={checkPermissions} />
    </Route>
  </Router>)
  }
});

3. המכוער: שינויי API מהירים מדי וחוסר גמישות

גירסא 0.13 של הספריה יצאה במרץ 2015. בנובמבר אותה השנה, רק 8 חודשים אחרי, יוצאת גירסא 1 עם שינויי API משמעותיים. בפברואר השנה, בקושי שלושה חודשים אחרי גירסא 1, יוצאת גירסא 2, שוב עם שינויי API משמעותיים.

זה המון שינויים לתקופה של פחות משנה.

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

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

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