Integrasi dengan Library Lain

React dapat digunakan pada aplikasi peramban apapun. React juga dapat ditanamkan di aplikasi lain, dan dengan sedikit pengaturan, aplikasi lain dapat ditanamkan di React. Panduan ini akan membahas beberapa kasus penggunaan yang lebih umum, memfokuskan pada integrasi dengan jQuery dan Backbone, tetapi ide yang sama dapat diaplikasikan untuk mengintegrasikan komponen dengan kode yang ada.

Integrasi dengan Plugin Manipulasi DOM

React tidak akan menyadari perubahan yang dilakukan pada DOM diluar dari React. Ini menentukan pembaharuan berdasarkan representasi internal sendiri, dan jika node DOM yang sama dimanipulasi oleh library lain, React menjadi bingung dan tidak memiliki cara untuk memulihkannya.

Ini tidak berarti tidak mungkin atau bahkan sulit untuk menggabungkan React dengan cara-cara lain untuk mempengaruhi DOM, Anda hanya perlu memperhatikan apa yang dilakukan oleh masing-masing.

Cara termudah untuk menghindari konflik adalah mencegah komponen React terbarui. Anda dapat melakukannya dengan me-render elemen-elemen yang React tidak punya alasan untuk mengubahnya, seperti sebuah <div /> kosong.

Cara Pendekatan Masalah

Untuk mendemonstrasikan ini, mari kita menggambarkan sebuah pembungkus untuk sebuah plugin generik jQuery.

Kita akan melampirkan ref kepada akar elemen DOM. Di dalam componentDidMount, kita akan mendapat sebuah referensi sehingga kita dapat menyampaikannya kepada plugin jQuery.

Untuk mencegah React menyentuh DOM setelah pemasangan, kami akan mengembalikan sebuah <div /> kosong dari method render(). Elemen <div /> tidak memiliki properti atau anak, sehingga React tidak memiliki alasan untuk memperbaruinya, meninggalkan plugin jQuery bebas untuk mengelola bagian dari DOM tersebut:

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

Perhatikan bahwa kami mendefinisikan keduanya componentDidMount dan componentWillUnmount lifecycle methods. Banyak plugin jQuery melampirkan pendengar event pada DOM sehingga ini penting untuk melepaskan mereka dalam componentWillUnmount. Jika plugin tidak menyediakan sebuah method untuk membersihkan, anda mungkin akan menyediakannya sendiri, ingat untuk menghapus pendengar event apapun yang didaftarkan ke plugin untuk mencegah kebocoran memori.

Integrasi dengan Plugin Chosen jQuery

Untuk contoh yang lebih konkret dari konsep-konsep ini, mari kita tulis pembungkus minimal untuk plugin Chosen, yang menambah masukan <select>.

Catatan:

Hanya karena hal tersebut memungkinan, bukan berarti itu adalah pendekatan yang terbaik untuk aplikasi React. Kami menganjurkan Anda untuk menggunakan komponen React saat Anda bisa. Komponen React lebih mudah untuk digunakan ulang dalam aplikasi React, dan sering kali menyediakan kontrol yang lebih pada perilaku dan tampilannya.

Pertama-tama, mari kita lihat apa yang Chosen lakukan pada DOM.

Jika Anda memanggilnya pada sebuah <select> node DOM, dia akan membaca attribut dari node DOM asli, menyembunyikannya dengan sebuah gaya inline, dan setelah itu menambahkan sebuah DOM node yang terpisah dengan representasi visualnya sendiri tepat setelah <select>. Setelahnya, dia akan memanggil event jQuery untuk memberitahu kita tentang perubahannya.

Anggaplah bahwa ini adalah API yang kita perjuangkan untuk komponen pembungkus <Chosen> React kita:

function Example() {
  return (
    <Chosen onChange={value => console.log(value)}>
      <option>vanilla</option>
      <option>chocolate</option>
      <option>strawberry</option>
    </Chosen>
  );
}

Kita akan mengimplementasikannya sebagai sebuah uncontrolled component untuk penyederhanaan.

Pertama, kita akan membuat komponen kosong dengan sebuah metode render() di mana kita mengembalikan <select> yang dibungkus di dalam sebuah <div>:

class Chosen extends React.Component {
  render() {
    return (
      <div>
        <select className="Chosen-select" ref={el => this.el = el}>
          {this.props.children}
        </select>
      </div>
    );
  }
}

Lihat bagaimana kita membungkus <select> dalam sebuah <div> ekstra. Ini penting karena Chosen akan menambahkan elemen DOM lainnya tepat setelah node <select> yang kita berikan padanya. Namun, sejauh yang React ketahui, <div> hanya selalu memiliki satu children. Ini adalah bagaimana kita memastikan pembaharuan React tidak akan konflik dengan ekstra node DOM yang ditambahkan Chosen. Ini juga penting bahwa jika anda memodifikasi DOM di luar alur React, anda harus memastikan React tidak punya alasan untuk menyentuh node DOM tersebut.

Selanjutnya, kita akan mengimplementasikan metode lifecycle. Kita akan menginisialisasi Chosen dengan ref kepada node <select> pada componentDidMount, dan menghancurkannya dalam componentWillUnmount:

componentDidMount() {
  this.$el = $(this.el);
  this.$el.chosen();
}

componentWillUnmount() {
  this.$el.chosen('destroy');
}

Coba di CodePen

Catat bahwa React tidak memberikan arti khusus kepada field this.el. Ini hanya berfungsi karena sebelumnya kita menugaskan field ini dari sebuah ref dalam metode render():

<select className="Chosen-select" ref={el => this.el = el}>

Ini cukup untuk mengambil komponen kita untuk di-render, tapi kita juga ingin diberitahu tentang perubahan nilai. Untuk melakukan ini, kita akan berlangganan pada jQuery event change di <select> yang diatur oleh Chosen.

Kita tidak akan mengoper this.props.onChange secara langsung pada Chosen karena props komponen mungkin berubah seiring waktu, dan itu termasuk event handler. Sebagai gantinya, kita akan mendeklarasikan sebuah metode handleChange() yang memanggil this.props.onChange, dan berlangganan pada event change di jQuery:

componentDidMount() {
  this.$el = $(this.el);
  this.$el.chosen();

  this.handleChange = this.handleChange.bind(this);
  this.$el.on('change', this.handleChange);
}

componentWillUnmount() {
  this.$el.off('change', this.handleChange);
  this.$el.chosen('destroy');
}

handleChange(e) {
  this.props.onChange(e.target.value);
}

Coba di CodePen

Akhirnya, tinggal satu hal lagi yang harus dilakukan. Di React, props bisa berganti seiring waktu. Contohnya, komponen <Chosen> dapat mengambil children yang berbeda jika komponen parent berganti state. Ini berarti pada poin integrasi ini sangatlah penting jika kita secara manual memperbarui DOM sebagai tanggapan pada pembaruan prop, karena kita tidak perlu membiarkan React mengatur DOM untuk kita.

Dokumentasi Chosen menyarankan kita untuk menggunakan API trigger() jQuery untuk memberitahu tentang perubahan pada DOM elemen yang asli. Kita akan membiarkan React mengurus pembaruan this.props.children di dalam <select>, tapi kita juga akan menambahkan sebuah metode lifecycle componentDidUpdate() yang memberitahu Chosen tentang perubahan di dalam daftar children:

componentDidUpdate(prevProps) {
  if (prevProps.children !== this.props.children) {
    this.$el.trigger("chosen:updated");
  }
}

Dengan cara ini, Chosen akan tahu untuk mengubah DOM elemennya saat <select> children diatur oleh perubahan pada React.

Implementasi lengkap dari komponen Chosen terlihat seperti ini:

class Chosen extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.chosen();

    this.handleChange = this.handleChange.bind(this);
    this.$el.on('change', this.handleChange);
  }
  
  componentDidUpdate(prevProps) {
    if (prevProps.children !== this.props.children) {
      this.$el.trigger("chosen:updated");
    }
  }

  componentWillUnmount() {
    this.$el.off('change', this.handleChange);
    this.$el.chosen('destroy');
  }
  
  handleChange(e) {
    this.props.onChange(e.target.value);
  }

  render() {
    return (
      <div>
        <select className="Chosen-select" ref={el => this.el = el}>
          {this.props.children}
        </select>
      </div>
    );
  }
}

Coba di CodePen

Integrasi dengan Library Tampilan Lain

React dapat ditanamkan pada aplikasi lain karena kefleksiblelannya ReactDOM.render().

Meskipun React umum digunakan di startup untuk memuat komponen akar tunggal React pada DOM, ReactDOM.render() juga bisa dipanggil beberapa kali untuk bagian independen dari UI yang di mana dapat berupa sekecil tombol, atau sebesar sebuah aplikasi.

Faktanya, seperti inilah bagaimana React digunakan di Facebook. Ini membuat kita menulis aplikasi pada React sedikit demi sedikit, dan mengkombinasikannya dengan templat yang dihasilkan server kami dan kode sisi klien lainnya.

Mengganti Rendering Berbasis String dengan React

Sebuah pola yang umum pada aplikasi peramban lama adalah untuk mendeskripsikan bingkah dari DOM sebagai string dan memasukannya kepada DOM seperti: $el.html(htmlString). Poin ini dalam sebuah basis kode sempurna untuk memperkenalkan React. Cukup tulis ulang rendering berbasis string sebagai komponen React.

Implementasi jQuery berikut ini…

$('#container').html('<button id="btn">Say Hello</button>');
$('#btn').click(function() {
  alert('Hello!');
});

…dapat ditulis ulang menggunakan sebuah komponen React:

function Button() {
  return <button id="btn">Say Hello</button>;
}

ReactDOM.render(
  <Button />,
  document.getElementById('container'),
  function() {
    $('#btn').click(function() {
      alert('Hello!');
    });
  }
);

Dari sini Anda dapat mulai memindahkan lebih banyak logika ke komponen dan mulai mengadopsi lebih banyak praktik React yang lebih umum. Contohnya, dalam komponen yang terbaik adalah untuk tidak bergantung pada ID karena komopnen yang sama dapat di-render beberapa kali. Sebagai gantinya, kira dapat menggunakan sistem event React dan meregistrasi handler klik langsung pada elemen <button> React:

function Button(props) {
  return <button onClick={props.onClick}>Say Hello</button>;
}

function HelloButton() {
  function handleClick() {
    alert('Hello!');
  }
  return <Button onClick={handleClick} />;
}

ReactDOM.render(
  <HelloButton />,
  document.getElementById('container')
);

Coba di CodePen

Anda dapat memiliki komponen yang terisolasi sebanyak yang anda suka, dan menggunakan ReactDOM.render() untuk merendernya pada kontainer DOM yang berbeda. Sedikit demi sedikit, saat Anda mengonversi lebih banyak bagian dari aplikasi Anda ke React, anda akan bisa mengkombinasikannya menjadi komponen yang lebih besar, dan memindahkan beberapa dari hirarki pemanggilan ReactDOM.render().

Menanamkan React dalam Tampilan Backbone

Tampilan Backbone secara khusus menggunakan string HTML, atau templat fungsi pembuat string, untuk membuat konten untuk elemen DOM mereka. Proses ini juga dapat digantikan dengan me-render sebuah komponen React.

Di bawah ini, kita dapat membuat sebuah tampilan Backbone bernama ParagraphView. Tampilan ini akan mengesampingkan fungsi render() Backbone untuk me-render sebuah komponen <Paragraph> React pada elemen DOM yang disediakan oleh Backbone (this.el). Di sini kita juga menggunakan ReactDOM.render():

function Paragraph(props) {
  return <p>{props.text}</p>;
}

const ParagraphView = Backbone.View.extend({
  render() {
    const text = this.model.get('text');
    ReactDOM.render(<Paragraph text={text} />, this.el);
    return this;
  },
  remove() {
    ReactDOM.unmountComponentAtNode(this.el);
    Backbone.View.prototype.remove.call(this);
  }
});

Coba di CodePen

Adalah penting bahwa kita juga dapat memanggil ReactDOM.unmountComponentAtNode() pada metode remove sehingga React membatalkan event handler registrasi dan sumber lainnya yang terkait dengan pohon komponen saat dicopot.

Saat sebuah komponen dihapus dari dalam sebuah pohon React, pembersihan dilakukan secara otomatis, tapi karena kita menghapus seluruh pohon secara manual, kita harus memanggil metode ini.

Integrasi dengan Lapisan Model

Meskipun pada umumnya dianjurkan untuk menggunakan aliran data searah seperti state React, Flux, atau Redux, komponen React juga dapat menggunakan lapisan model dari framework dan library lainnya.

Menggunakan Model Backbone pada Komponen React

Cara termudah untuk menggunakan model Backbone dan koleksi dari sebuah komponen React adalah dengan mendengarkan berbagai macam perubahan event dan memaksakan sebuah perubahan secara manual.

Komponen yang bertanggung jawab untuk me-render model akan mendengarkan event 'change', sementara komponen yang bertanggung jawab untuk me-render koleksi akan mendengarkan event 'add' dan 'remove'. Pada kedua kasus tersebut, panggil this.forceUpdate() untuk me-render ulang komponen dengan data yang baru.

Pada contoh dibawah ini, komponen List me-render sebuah koleksi backbone, menggunakan komponen Item untuk me-render item individual.

class Item extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange() {
    this.forceUpdate();
  }

  componentDidMount() {
    this.props.model.on('change', this.handleChange);
  }

  componentWillUnmount() {
    this.props.model.off('change', this.handleChange);
  }

  render() {
    return <li>{this.props.model.get('text')}</li>;
  }
}

class List extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange() {
    this.forceUpdate();
  }

  componentDidMount() {
    this.props.collection.on('add', 'remove', this.handleChange);
  }

  componentWillUnmount() {
    this.props.collection.off('add', 'remove', this.handleChange);
  }

  render() {
    return (
      <ul>
        {this.props.collection.map(model => (
          <Item key={model.cid} model={model} />
        ))}
      </ul>
    );
  }
}

Coba di CodePen

Mengekstrak Data dari Model Backbone

Pendekatan di atas membutuhkan komponen React Anda mengetahui model dan koleksi Backbone. Jika Anda nantinya merencanakan untuk melakukan migrasi ke solusi manajemen data lainnya, Anda mungkin ingin memusatkan pengetahuan tentang Backbone hanya di sebagian kecil kode.

Salah satu solusi untuk ini adalah untuk mengekstrak atribut model sebagai data polos setiap kali terjadi perubahan, dan menjaga logika ini pada satu tempat. Berikut ini adalah higher-order component yang mengekstrak seluruh atribut dari sebuah model Backbone pada state, mengoper data pada komponen yang dibungkus.

Dengan ini, hanya higher-order component yang perlu tahu tentang model internal Backbone, dan sebagian besar komponen di aplikasi dapat tetap terpisah dari Backbone.

Pada contoh dibawah ini, kita akan membuat sebuah salinan dari atribut model untuk membentuk state awal. Kita berlangganan pada event change (dan berhenti berlangganan saat sedang melakukan unmount), dan saat itu terjadi, kita ubah state dengan atribut model saat ini. Akhirnya, kita pastikan jika prop model itu sendiri berubah, kita tidak lupa untuk berhenti berlangganan dari model yang lama, dan berlangganan pada model yang baru.

Catat bahwa contoh ini tidak dimaksudkan sebagai contoh yang menyeluruh untuk bekerja dengan Backbone. Tapi ini seharusnya memberi anda sebuah gagasan tentang bagaimana cara mendekatinya dengan cara yang umum:

function connectToBackboneModel(WrappedComponent) {
  return class BackboneComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = Object.assign({}, props.model.attributes);
      this.handleChange = this.handleChange.bind(this);
    }

    componentDidMount() {
      this.props.model.on('change', this.handleChange);
    }

    componentWillReceiveProps(nextProps) {
      this.setState(Object.assign({}, nextProps.model.attributes));
      if (nextProps.model !== this.props.model) {
        this.props.model.off('change', this.handleChange);
        nextProps.model.on('change', this.handleChange);
      }
    }

    componentWillUnmount() {
      this.props.model.off('change', this.handleChange);
    }

    handleChange(model) {
      this.setState(model.changedAttributes());
    }

    render() {
      const propsExceptModel = Object.assign({}, this.props);
      delete propsExceptModel.model;
      return <WrappedComponent {...propsExceptModel} {...this.state} />;
    }
  }
}

Untuk mendemonstrasikan bagaimana cara menggunakannya, kita akan menghubungkan sebuah komponen React NameInput pada sebuah model Backbone, dan memperbarui atribut firstName setiap kali terjadi perubahan masukan:

function NameInput(props) {
  return (
    <p>
      <input value={props.firstName} onChange={props.handleChange} />
      <br />
      My name is {props.firstName}.
    </p>
  );
}

const BackboneNameInput = connectToBackboneModel(NameInput);

function Example(props) {
  function handleChange(e) {
    props.model.set('firstName', e.target.value);
  }

  return (
    <BackboneNameInput
      model={props.model}
      handleChange={handleChange}
    />
  );
}

const model = new Backbone.Model({ firstName: 'Frodo' });
ReactDOM.render(
  <Example model={model} />,
  document.getElementById('root')
);

Coba di CodePen

Teknik ini tidak terbatas pada Backbone. Anda dapat menggunakan React dengan library model apapun dengan berlangganan pada perubahannya di metode lifecycle, dan, secara opsional, menyalin data pada state lokal React.