• בלוג
  • גם בריאקט: לא תמיד קל להגיד לילדים מה לעשות

גם בריאקט: לא תמיד קל להגיד לילדים מה לעשות

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

שתי צורות של תקשורת בריאקט מאוד ברורות מהתיעוד: הראשונה היא העברת מידע מהורה לילד באמצעות Properties, והשניה היא שליחת פקודות מילד להורה באמצעות העברת Callback בתור Property. פוסט זה יעסוק בבעיה שלישית שפחות מדברים עליה: איך לשלוח פקודה מהורה לילד.

1. למה בכלל צריך

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

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

למי שקורא את זה מהמייל זה קוד הפקד המלא (אבל באמת לכו לקרוא מהאתר זה יותר כיף כשהפקד פעיל):

class CustomInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = { val: '    ' };
    this.el = [];
  }

  keyPressed = (index, e) => {    
    const charcode = e.charCode;
    const char = String.fromCharCode(charcode);
    if (char.match(/[0-9a-zA-Z]/)) {
      e.preventDefault();

      const arr = this.state.val.split('');

      arr[index] = char;
      this.setState(() => ({ val: arr.join('') }));
      this.el[(index + 1) % arr.length].focus();
    }
  }

  render() {
    return (
      <div>
        {this.state.val.split('').map((val, index) => (
          <div
            tabindex='1'
            className='char'
            key={index}
            onKeyPress={e => this.keyPressed(index, e)}
            ref={(el) => this.el[index] = el}
            >
            {val}
          </div>
        ))}        
      </div>
    );
  }
}

ReactDOM.render(<CustomInput />, document.querySelector('main'));

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

ניסיון ראשון לשילוב יכול להיות משהו כזה:

class App extends React.Component {
  render() {
    return (
      <div>
        <CustomInput />
        <CustomInput />
        <CustomInput />
      </div>
    );
  }
}

אבל עכשיו איך נודיע לכל CustomInput שהגיע תורו לקבל פוקוס כשהקודם מסיים? במילים אחרות נניח שכל ילד יכול להודיע ל App כשהוא מסיים את הרביעייה שלו, מה App יעשה כשיקבל את ההודעה? הפקד App לא מכיר את ה DOM Elements הספציפיים של כל אחד מהילדים שלו, ולכן לא יכול לקרוא ל focus שלהם באופן יזום:

class App extends React.Component {
  constructor(props) {
    super(props);
  }

  done = () => {
    // ???
  }

  render() {
    return (
      <div>
        <CustomInput done={this.done} />
        <CustomInput done={this.done} />
        <CustomInput done={this.done} />
      </div>
    );
  }
}

2. כיוון אחד: ניהול סטייט בפקד App והודעה באמצעות componentDidUpdate

אפשרות ראשונה היא לנהל ב State של App את המידע שרלוונטי לילדים שלו, בפרט איזה מהם כרגע פעיל. נעביר לילדים את המידע דרך Property מתאים וכל ילד ״יתפוס״ את השינוי באמצעות הפונקציה componentDidUpdate. כך נראה הקוד:

class CustomInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = { val: '    ' };
    this.el = [];
  }

  componentDidUpdate(prevProps, prevState) {
    if (!prevProps.active && this.props.active) {
      this.el[0].focus();
    }
  }

  keyPressed = (index, e) => {    
    const charcode = e.charCode;
    const char = String.fromCharCode(charcode);
    if (char.match(/[0-9a-zA-Z]/)) {
      e.preventDefault();

      const arr = this.state.val.split('');

      arr[index] = char;
      this.setState(() => ({ val: arr.join('') }));
      if (index === arr.length - 1) {
        this.props.done();
      } else {
        this.el[(index + 1) % arr.length].focus();
      }      
    }
  }

  render() {
    return (
      <div>
        {this.state.val.split('').map((val, index) => (
          <div
            tabindex='1'
            className='char'
            key={index}
            onKeyPress={e => this.keyPressed(index, e)}
            ref={(el) => this.el[index] = el}
            >
            {val}
          </div>
        ))}        
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { active: 0 };
  }

  done = () => {
    this.setState((oldState => ({ active: (oldState.active + 1) % 3 })));
  };

  render() {
    return (
      <div>
        <CustomInput active={this.state.active === 0} done={this.done} />
        <CustomInput active={this.state.active === 1} done={this.done} />
        <CustomInput active={this.state.active === 2} done={this.done} />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector('main'));

או בגירסת לייב:

ברגע שרכיב סיים את עבודתו הוא מעביר הודעה done ל App. שינוי ב App יביא לשינוי ב Properties שייתפס ב componentDidUpdate ושם אפשר לשנות פוקוס מקלדת (או לבצע כל פעולה אחרת).

3. כיוון שני: העברת ה Ref הרלוונטים ל App

דרך אחרת היא להעביר ל App את כל המידע שהוא צריך כדי לבצע את הפעולה, וכך העברת הפוקוס תתבצע בפקד זה (ולא בילדים). זה אומר שאנחנו צריכים לשמור מערך של DOM Elements ב App ובכל פעם שפקד ילד מספר שסיים נקרא ל focus על ה DOM Element הבא. כך נראה הקוד:

class CustomInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = { val: '    ' };
    this.el = [];
  }

  keyPressed = (index, e) => {    
    const charcode = e.charCode;
    const char = String.fromCharCode(charcode);
    if (char.match(/[0-9a-zA-Z]/)) {
      e.preventDefault();

      const arr = this.state.val.split('');

      arr[index] = char;
      this.setState(() => ({ val: arr.join('') }));
      if (index === arr.length - 1) {
        this.props.done();
      } else {
        this.el[(index + 1) % arr.length].focus();
      }      
    }
  }

  render() {
    return (
      <div>
        {this.state.val.split('').map((val, index) => (
          <div
            tabindex='1'
            className='char'
            key={index}
            onKeyPress={e => this.keyPressed(index, e)}
            ref={index === 0 ? this.props.myRef : (el) => this.el[index] = el}
            >
            {val}
          </div>
        ))}        
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.items = [];
  }

  done(idx) {
    this.items[(idx + 1) % this.items.length].focus();
  };

  render() {
    return (
      <div>
        <CustomInput myRef={(el) => { this.items[0] = el; }} done={this.done.bind(this, 0)} />
        <CustomInput myRef={(el) => { this.items[1] = el; }} done={this.done.bind(this, 1)} />
        <CustomInput myRef={(el) => { this.items[2] = el; }} done={this.done.bind(this, 2)} />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector('main'));

או בגירסת לייב בקודפן:

4. מסקנות: איך שולחים פקודה מהורה לילד ב React

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

דרך שניה היא לעשות את העבודה בשבילם. זה מה שרינו בדוגמא השניה שם שמרנו בפקד App את כל המידע לצורך ביצוע מעבר הפוקוס ובפונקציה done ביצענו את המעבר כבר ב App.