Mengoper fungsi-fungsi ke dalam komponen

Bagaimana saya mengoper event handler (seperti onClick) ke sebuah komponen? {#how-do-i-pass-an-event-handler-like-onclick-to-a-component }

Anda bisa melakukannya dengan cara meletakkan event handlers dan fungsi-fungsi lain sebagai props untuk child components:

<button onClick={this.handleClick}>

Jika Anda butuh akses ke parent component pada handler tersebut, maka Anda juga harus mem-bind fungsi tersebut pada component instance (lihat di bawah).

Bagaimana saya mem-bind fungsi ke sebuah component instance

Ada beberapa cara untuk membuat fungsi memiliki akses terhadap atribut komponen seperti this.props dan this.state, tergantung pada sintaksis mana dan build steps seperti apa yang Anda gunakan.

Bind di dalam Konstruktor (ES2015)

class Foo extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <button onClick={this.handleClick}>Click Me</button>;
  }
}

Class Properties (Stage 3 Proposal)

class Foo extends Component {
  // Note: this syntax is experimental and not standardized yet.
  handleClick = () => {
    console.log('Click happened');
  }
  render() {
    return <button onClick={this.handleClick}>Click Me</button>;
  }
}

Bind di dalam Render

class Foo extends Component {
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <button onClick={this.handleClick.bind(this)}>Click Me</button>;
  }
}

Catatan:**

Menggunakan Function.prototype.bind di dalam render akan membuat fungsi baru setiap kali komponen ter-render, yang akan mempengaruhi performa (lihat di bawah).

Arrow Function dalam Method Render

class Foo extends Component {
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <button onClick={() => this.handleClick()}>Click Me</button>;
  }
}

Catatan:**

Menggunakan arrow function di dalam method render akan menyebabkan program membuat fungsi baru setiap kali component ter-render. Hal ini akan berpengaruh terhadap performa (akan dibahas di bagian selanjutnya)

Apakah tidak ada masalah menggunakan arrow function di metode render

Secara umum tidak ada masalah, dan terkadang inilah cara termudah untuk mengoper parameter pada fungsi callback.

Adapun demikian, jika Anda menemukan masalah dengan performa program, anda perlu melakukan optimasi.

Mengapa binding diperlukan?

Dalam JavaScript, kedua contoh kode ini tidak sama (memiliki makna berbeda):

obj.method();
var method = obj.method;
method();

Melakukan binding pada method membantu kita untuk memastikan bahwa contoh kode kedua akan berjalan persis seperti contoh kode pertama.

Pada React, biasanya Anda hanya perlu melakukan binding method jika method tersebut diberikan pada komponen lain. Sebagai contoh, <button onClick={this.handleClick}> mengoper this.handleClick pada komponen button, sehingga Anda perlu melakukan binding. Adapun demikian, kita tidak perlu melakukan binding method render atau binding method-method bawaan lainnya (lifecycle method) karena kita tidak mengoper method-method tersebut pada komponen lain.

Artikel oleh Yehuda Katz menjelaskan apa itu binding, dan bagaimana cara kerja fungsi di dalam JavaScript secara detail.

Mengapa fungsi saya dijalankan setiap kali component di-render (bukan saat event terkait)?

Pastikan anda tidak menjalankan fungsi saat mengopernya pada component (Perhatikan tanda kurungnya):

render() {
  // Wrong: handleClick is called instead of passed as a reference!
  return <button onClick={this.handleClick()}>Click Me</button>
}

Cara yang benar untuk mengoper fungsi adalah sebagai berikut (tanpa tanda kurung):

render() {
  // Correct: handleClick is passed as a reference!
  return <button onClick={this.handleClick}>Click Me</button>
}

Bagaimana saya mengoper parameter pada sebuah event handler atau callback?

Anda bisa menggunakan arrow function untuk membungkus event handler dan mengoper parameter:

<button onClick={() => this.handleClick(id)} />

Hal tersebut sama dengan melakukan .bind:

<button onClick={this.handleClick.bind(this, id)} />

Contoh: Mengoper parameter menggunakan arrow function

const A = 65 // ASCII character code

class Alphabet extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.state = {
      justClicked: null,
      letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
    };
  }
  handleClick(letter) {
    this.setState({ justClicked: letter });
  }
  render() {
    return (
      <div>
        Just clicked: {this.state.justClicked}
        <ul>
          {this.state.letters.map(letter =>
            <li key={letter} onClick={() => this.handleClick(letter)}>
              {letter}
            </li>
          )}
        </ul>
      </div>
    )
  }
}

Contoh: Mengoper parameter menggunakan data-attributes

Cara yang lain, anda bisa memakai DOM API untuk menyimpan data yang dibutuhkan untuk event handler. Pertimbagnkan cara ini jika anda harus mengoptimasi element dalam jumlah besar atau anda harus merender struktur tree yang tergantung pada pengecekan kesamaan React.PureComponent

const A = 65 // ASCII character code

class Alphabet extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.state = {
      justClicked: null,
      letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
    };
  }

  handleClick(e) {
    this.setState({
      justClicked: e.target.dataset.letter
    });
  }

  render() {
    return (
      <div>
        Just clicked: {this.state.justClicked}
        <ul>
          {this.state.letters.map(letter =>
            <li key={letter} data-letter={letter} onClick={this.handleClick}>
              {letter}
            </li>
          )}
        </ul>
      </div>
    )
  }
}

Bagaimana saya menghindari sebuah fungsi dipanggil terlalu cepat atau terpanggil berkali-kali secara berurutan?

Jika anda memiliki event handler seperti onClick atau onScroll dan anda ingin mencegah callback dari event tersebut terpanggil terlalu cepat, maka anda dapat melakukan pembatasan kecepatan pemanggilan fungsi callback. Hal tersebut dapat dilakukan menggunakan:

  • throttling: Mengecek apakah ada perubahan dalam jangka waktu tertentu (contoh: _.throttle)
  • debouncing: Mempublish perubahan setelah tidak ada aktivitas selama beberapa waktu (contoh: _.debounce)
  • requestAnimationFrame throttling: Mengecek perubahan berdasarkan requestAnimationFrame (eg raf-schd)

Silahkan melihat gambar berikut untuk mendapatkan perbandingan antara throttle dan debounce.

Catatan:

_.debounce, _.throttle dan raf-schd menyediakan metode cancel untuk membatalkan callback yang tertunda. Anda harus memanggil metode melalui componentWillUnmount atau melakukan pengecekan untuk memastikan bahwa komponen yang dimasuksu masih dalam keadaan ter-mount selama penundaan eksekusi fungsi callback.

Throttle

Teknik throttling mencegah sebuah fungsi terpanggil beberapa kali dalam jangka waktu tertentu. Sebagai contoh, berikut adalah contoh throttle pada handler “click” untuk mencegah pemanggilan lebih dari sekali dalam waktu satu detik.

import throttle from 'lodash.throttle';

class LoadMoreButton extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.handleClickThrottled = throttle(this.handleClick, 1000);
  }

  componentWillUnmount() {
    this.handleClickThrottled.cancel();
  }

  render() {
    return <button onClick={this.handleClickThrottled}>Load More</button>;
  }

  handleClick() {
    this.props.loadMore();
  }
}

Debounce

Teknik debouncing memastikan bahwa sebuah fungsi tidak akan dieksekusi setelah beberapa waktu sejak pemanggilan terakhir. Teknik ini akan berguna terutama jika kita harus melakukan perhitungan yang berat sebagai respon terhadap event yang terjadi berkali-kali dalam waktu yang singkat (misalnya scroll atau penekanan tombol keyboard). Berikut adalah contoh teknik debounce pada text input dengan penundaan sebesar 250 ms.

import debounce from 'lodash.debounce';

class Searchbox extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.emitChangeDebounced = debounce(this.emitChange, 250);
  }

  componentWillUnmount() {
    this.emitChangeDebounced.cancel();
  }

  render() {
    return (
      <input
        type="text"
        onChange={this.handleChange}
        placeholder="Search..."
        defaultValue={this.props.value}
      />
    );
  }

  handleChange(e) {
    // React pools events, so we read the value before debounce.
    // Alternately we could call `event.persist()` and pass the entire event.
    // For more info see reactjs.org/docs/events.html#event-pooling
    this.emitChangeDebounced(e.target.value);
  }

  emitChange(value) {
    this.props.onChange(value);
  }
}

requestAnimationFrame throttling

requestAnimationFrame adalah sebuah cara untuk mengantrikan fungsi sehingga bisa dieksekusi oleh browser secara optimal untuk rendering. Sebuah fungsi yang diantrikan menggunakan requestAnimationFrame akan dijalankan pada frame selanjutnya. Secara normal, browser akan bekerja untuk memastikan bahwa akan ada 60 frame dalam waktu satu detik (60 fps). Namun, jika browser tidak mampu memenuhi standar tersebut, maka browser akan membatasi jumlah frame dalam satu detik. Sebagai contoh, sebuah device mungkin hanya mampu untuk melayani 30 fps. Menggunakan requestAnimationFrame untuk throttling adalah teknik yang berguna untuk membatasi supaya tidak ada lebih dari 60 perubahan dalam satu detik. Jika anda melakukan 100 perubahan dalam satu detik, hal tersebut hanya akan membuat browser melakukan komputasi yang pada dasarnya tidak akan terlihat dari sisi user. Teknik throttling digunakan untuk mencegah hal tersebut.

Catatan:

Dengan menggunakan teknik ini, maka peramban hanya akan mengolah perubahan terakhir dalam satu frame. Anda dapat melihat contoh detil optimasinya di MDN

import rafSchedule from 'raf-schd';

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

    this.handleScroll = this.handleScroll.bind(this);

    // Create a new function to schedule updates.
    this.scheduleUpdate = rafSchedule(
      point => this.props.onScroll(point)
    );
  }

  handleScroll(e) {
    // When we receive a scroll event, schedule an update.
    // If we receive many updates within a frame, we'll only publish the latest value.
    this.scheduleUpdate({ x: e.clientX, y: e.clientY });
  }

  componentWillUnmount() {
    // Cancel any pending updates since we're unmounting.
    this.scheduleUpdate.cancel();
  }

  render() {
    return (
      <div
        style={{ overflow: 'scroll' }}
        onScroll={this.handleScroll}
      >
        <img src="/my-huge-image.jpg" />
      </div>
    );
  }
}

Menguji pembatasan rate

Saat menguji apakah pembatasan rate yang Anda terapkan sudah bekerja dengan baik, akan sangat membantu jika kita bisa mempercepat waktu. Jika Anda menggunakan jest maka Anda bisa menggunakan mock timers untuk mempercepat waktu. Jika anda menggunakan pelambatan requestAnimationFrame maka raf-stub juga akan berguna untuk mengendalikan jumlah frame per detik.