פיתוח טפסים חכמים עם ריילס, ריאקט ו Context API

11/11/2018

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

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

1. מה אנחנו בונים

בתוך דפי ERB המייצרים דפי HTML בריילס אפשר להשתמש במבנה הבא כדי לקבל טופס:

<%= form_with(model: user, local: true) do |form| %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

וזה עובד ומייצר טופס שנראה כך:

<form action="/users" accept-charset="UTF-8" method="post" _lpchecked="1">
    <input name="utf8" type="hidden" value="✓">
    <input type="hidden" name="authenticity_token" value="uTS/LJtv2QwgoYlM9phOh4v9Dni9b0jmM/jSQNhxLk/9m8ouKeRT2RHpFxjefLnay9W4O7e1HIkVGgCUf/OqIA==">

  <div class="field">
    <label for="user_name">Name</label>
    <input type="text" name="user[name]" id="user_name">
  </div>

  <div class="field">
    <label for="user_email">Email</label>
    <input type="text" name="user[email]" id="user_email">
  </div>

  <div class="actions">
    <input type="submit" name="commit" value="Create User" data-disable-with="Create User">
  </div>
</form>

קיבלנו שני שדות נסתרים (אחד שנקרא utf8 והשני נקרא authenticity_token) והגדרה אוטומטית של מאפיין name לכל אחד מה inputs.

2. ייצור טופס מקביל בריאקט

בשביל להפוך את הקוד לריאקט נרצה להגדיר טופס עם הקוד הבא:

<Form action="user">
    <div className='div'>
        <Label htmlFor="name" />
        <TextField name="name" />
    </div>

    <div className='div'>
        <Label htmlFor="email" />
        <EmailField name="email" />
    </div>
</Form>

ומה שהיינו רוצים לקבל זה את שני השדות הנסתרים ובנוסף אליהם נרצה הגדרה אוטומטית של השמות, כך ששדה האימייל יקבל בתור name את הערך user[email] ושדה השם יקבל בתור ה name שלו את הערך user[name]. שמות אלה נדרשים כדי לקבל תאימות עם ריילס כך שבצד השרת יהיה קל לשלוף את המידע ולייצר אוביקט User מתאים.

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

<meta name="csrf-token" content="BBIaSDxCY8UVgPF/ME/ykiItw4I5c0FSpe/Z/F+Q0w5CJiVIEJane397GWOFpZyI+8OC768cg9UkUm29CM1NAQ==">

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

import React from "react"
import PropTypes from "prop-types"

class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = { token: '' };
  }

  componentDidMount() {
    const token = document.querySelector('meta[name="csrf-token"]').content;
    this.setState({ token });
  }

  render () {
    return (
      <form method="POST" action={this.props.for}>
        <input type="hidden" value={this.state.token} />
        {this.props.children}
      </form>
    );
  }
}

export default Form

אבל מה הלאה? איך נכתוב את הפקדים TextField ו EmailField כדי להתאים אוטומטית את השמות? במילים אחרות - איך Form יכול להעביר לכל אחד מהילדים שלו בצורה מקוננת את התחילית שעברה רק אליו?

3. איך React Context API עוזר לפתור את הבעיה

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

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

בשביל להשתמש בקונטקסט יש לבצע:

  1. יש לייצר ולייצא אוביקט מסוג Context שיישמר בתור Singleton במקום שלכולם תהיה גישה אליו.

  2. יש לצרף לעץ הפקדים שלנו פקד מסוג Provider שנלקח מה Context שיצרנו.

  3. יש להעביר בתור מאפיין value של ה Provider את הערך שרוצים לשתף עם כל תת העץ.

  4. יש להוסיף מאפיין סטטי בשם contextType למחלקה שקוראת את ה Context ושם להעביר את ה Context שיצרנו בתור ערך.

נראה את ארבעת הצעדים על קוד הדוגמא. נתחיל בהוספת ה Context בקובץ form.js שמגדיר את פקד הטופס:

const formContext = React.createContext({ for: '' });
export { formContext };

הערך בסוגריים נקרא Default Value והוא משמש בתור ערך לקונטקסט אם אף אחד לא העביר ערך בצורה יזומה.

נמשיך לעדכון render ושם נוסיף את ה Provider בתוך עץ הפקדים:

  render () {
    const ctx = { for: this.props.for };

    return (
      <form method="POST" action={this.props.for}>
        <formContext.Provider value={ctx}>
          <input type="hidden" value={this.state.token} />
          {this.props.children}
        </formContext.Provider>
      </form>
    );
  }

עכשיו כל הילדים של Form יכולים לגשת לשדה for שלו, ובנוסף כששדה זה ישתנה באופן אוטומטי כל הילדים שמושפעים ממנו יעברו render מחדש.

נסיים בכתיבת הפקד TextField כך שישתמש בקונטקס:

import React from 'react';
import { formContext } from './form';

export default class TextField extends React.Component {
  static contextType = formContext;
  render() {
    const prefix = this.context.for;

    return <input type="text" name={`${prefix}[${this.props.name}]`} />
  }
}

והתוצאה ב HTML היא טופס שנראה כך:

<form method="POST" action="user">
    <input type="hidden" value="C40YS6UbIVvhLLPpzCbKhvaPOKuN4qke22hUDVh1v+lNuSdLic/l5YvXW/V5zKScL2F5xhuNa5la1eBMDygh5g==">
    <input type="text" name="user[name]">
</form>

4. סגירת קצוות

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

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

https://reactjs.org/docs/context.html

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

https://github.com/ynonp/react-rails-forms-demo

והכי חשוב- ה Context API הנוכחי הוא גילגול של ממשק ישן יותר ונכנס לשימוש רשמי ב React 16.3. בחמישי הקרוב אעביר וובינר על API זה וגם על APIs נוספים שהתחדשו בריאקט 16. מוזמנים להצטרף בחינם בקישור:

https://www.tocode.co.il/workshops/54