Tích hợp các thư viện bên ngoài
React có thể được sử dụng trong bất kỳ app web nào. Nó có thể được nhúng vào các app khác, và ngược lại, những app khác cũng có thể được nhúng vào trong React. Bài viết này sẽ đi vào một vài trường hợp điển hình, tập trung vào việc tích hợp jQuery và Backbone, nhưng với cách làm tương tự có thể được áp dụng để tích hợp components với bất kỳ loại code khác.
Tích hợp Plugins thao tác DOM
React không nhận biết được những sự thay đổi của DOM nếu DOM được tác động từ bên ngoài. Việc quyết định update hay không sẽ dựa trên chính những thành phần đại diện bên trong nó, và nếu những DOM node này được thay đổi bởi một thứ viện khác, React sẽ cảm thấy khó hiểu và không có cách nào để xử lý.
Điều này không có nghĩa là không thể hoặc rất khó để kết hợp React với những cách thao tác DOM khác, bạn chỉ cần chú ý mỗi phần riêng biệt sẽ làm gì.
Cách dễ nhất để tránh xung đột là ngăn chặn React component khỏi việc update. Bạn có thể làm việc này bằng cách render các element mà React không có lý do gì để update, ví dụ như một thẻ div trống <div />
.
Tiếp cận vấn đề
Để chứng minh điều trên, hãy bắt đầu với một đoạn wrapper của plugin jQuery
Chúng ta sẽ gán ref vào phần tử root DOM. Bên trong componentDidMount
, chúng ta sẽ tham chiếu đến nó và có thể truyền nó đến plugin jQuery.
Để ngăn chặn React động vào DOM sau khi được mount, chúng ta sẽ trả về một <div />
trống từ method render()
. Bởi vì phần tử <div />
không có thuộc tính hay phần tử con, nên React sẽ không có lí do gì để update nó, để cho jQuery plugin được thoải mái quản lý phần đó của DOM:
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} />; }
}
Chú ý rằng chúng ta sử dụng phương thức vòng đời componentDidMount
và componentWillUnmount
. Nhiều plugin jQuery chứa các event listener của DOM nên việc tháo những event listener trong componentWillUnmount
là quan trọng. Nếu những plugin này không cung cấp method cho việc tháo gỡ, có thể bạn phải tự mình làm việc đó, hãy nhớ xóa hết các event listener của plugin để ngăn chặn tràn bộ nhớ.
Tích hợp với một plugin jQuery cụ thể
Để có một ví dụ chắc chắn hơn cho những ý tưởng này, hãy làm một wrapper nhỏ cho plugin Chosen, một plugin hỗ trợ input <select>
.
Lưu ý:
Chỉ vì điều này có thể, không có nghĩa rằng đó là cách tiếp cận tốt nhất của các app React. Chúng tôi khuyến khích bạn sử dụng các component React khi có thể. Các component React dễ dàng được tái sử dụng hơn trong các app React, và thường cung cấp nhiều hơn khả năng điều khiển các hành động và hiển thị của nó.
Đầu tiên, hãy xem plugin Chosen làm gì với DOM.
Nếu bạn gọi nó trên một DOM node <select>
, nó sẽ đọc các attribute của DOM node ban đầu, ẩn DOM này bằng inline style, và sau đó thêm một DOM node riêng biệt có hiển thị của riêng nó ngay sau <select>
. Sau đó nó kích hoạt sự kiện jQuery để thông báo chúng ta về sự thay đổi.
Hãy cho rằng đây là API chúng ta đang sử dụng với component wrapper <Chosen>
function Example() {
return (
<Chosen onChange={value => console.log(value)}>
<option>vanilla</option>
<option>chocolate</option>
<option>strawberry</option>
</Chosen>
);
}
Chúng ta sẽ đơn giản thực thi nó như là một uncontrolled component.
Đầu tiên, chúng ta sẽ tạo một component trống với method render()
trả về <select>
được bọc trong <div>
:
class Chosen extends React.Component {
render() {
return (
<div> <select className="Chosen-select" ref={el => this.el = el}> {this.props.children}
</select>
</div>
);
}
}
Để ý cách chúng ta bọc <select>
trong một thẻ <div>
bổ sung. Điều này là cần thiết bởi vì Chosen sẽ thêm một phần tử DOM khác ngay sau <select>
chúng ta truyền vào. Tuy nhiên, theo như những gì React nhận biết, <div>
chỉ luôn luôn có một phần tử con. Đây là cách chúng ta chắc chắn rằng những update sẽ không gây xung đột với phần tử DOM được thêm bởi Chosen. Điều đó là quan trọng nếu bạn chỉnh sửa DOM bên ngoài React, bạn phải chắc chắn rằng React không có lí do động vào những DOM đó.
Tiếp theo, chúng ta sẽ cần tiến hành các phương thức lifecycle. Chúng ta cần khởi tạo Chosen gán ref cho <select>
bên trong componentDidMount
, và xóa nó đi trong componentWillUnmount
:
componentDidMount() {
this.$el = $(this.el); this.$el.chosen();}
componentWillUnmount() {
this.$el.chosen('destroy');}
Lưu ý rằng React không gán ý nghĩa đặc biệt nào cho field this.el
. Nó chỉ hoạt động bởi vì chúng ta đã gán cho nó một ref
trong method render()
:
<select className="Chosen-select" ref={el => this.el = el}>
Chừng này là đủ để cho component của chúng ta render, nhưng chúng ta cũng muốn được thông báo về sự thay đổi giá trị. Để làm việc này, chúng ta sẽ theo dõi sự kiện change
của jQuery trên <select>
- thẻ được quản lý bởi Chosen.
Chúng ta sẽ không truyền this.props.onChange
trực tiếp đến Chosen bởi vì những props của component có thể thay đổi liên tục, và nó bao gồm cả những hàm xử lý sự kiện. Thay vì thế, chúng ta sẽ định nghĩa một method handleChange()
gọi this.props.onChange
, và theo dõi nó thông qua sự kiện change
của 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);}
Cuối cùng, còn một việc nữa để làm. Trong React, props có thể thay đổi liên tục. Ví dụ, component <Chosen>
có thể có những children khác nhau nếu state của component cha thay đổi. Điều này có nghĩa khi tích hợp, việc này rất quan trọng khi chúng ta update DOM một cách thủ công trong trong việc phản hồi các update của prop, bởi vì chúng ta không còn để React quản lý DOM giúp chúng ta nữa.
Tài liệu của Chosen bảo rằng chúng ta có thể sử dụng jQuery API trigger()
để thông báo về những thay đổi trong DOM ban đầu. Chúng ta sẽ để React lo phần update this.props.children
bên trong <select>
, nhưng chúng ta cũng sẽ thêm method vòng đời componentDidUpdate()
để thông báo cho Chosen về những thay đổi trong children list:
componentDidUpdate(prevProps) {
if (prevProps.children !== this.props.children) { this.$el.trigger("chosen:updated"); }
}
Bằng cách này, Chosen sẽ biết để update DOM của nó khi children <select>
được thay đổi bởi React.
Cách vận hành đầy đủ của component Chosen
sẽ trông như thế này:
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>
);
}
}
Tích hợp những thư viện View khác
React có thể được nhúng vào bên trong các app khác nhờ vào sự linh hoạt của ReactDOM.render()
.
Mặc dù React thường được sử dụng ban đầu để thêm một component root vào DOM, ReactDOM.render()
cũng có thể được gọi nhiều lần cho những phần độc lập của UI, những thứ nhỏ như một button, hoặc lớn như một ứng dụng.
Thực tế, đây là chính xác cách mà React được sử dụng trong Facebook. Nó để chúng tôi viết từng phần nhỏ app trong React, và kết hợp chúng với những template được tạo bởi server có sẵn của chúng tôi và những đoạn code khác trên phần client.
Thay thế String-Based Rendering với React
Một pattern phổ biến trên những ứng dụng cũ là viết các phần của DOM như là một string và thêm nó vào DOM chẳng hạn như: $el.html(htmlString)
. Những điểm này trong một codebase là hoàn hảo cho việc sử dụng React. Chỉ cần viết lại string based rendering như một component React.
Hãy theo dõi cách sử dụng jQuery sau…
$('#container').html('<button id="btn">Say Hello</button>');
$('#btn').click(function() {
alert('Hello!');
});
…có thể được viết lại sử dụng một component React:
function Button() {
return <button id="btn">Say Hello</button>;
}
ReactDOM.render(
<Button />,
document.getElementById('container'),
function() {
$('#btn').click(function() {
alert('Hello!');
});
}
);
Từ đây bạn có thể bắt đầu sử dụng nhiều logic hơn với component và áp dụng nhiều React practices hơn. Ví dụ, trong những component, việc không phụ thuộc vào IDs là tốt nhất bởi vì một component tương tự không thể được render nhiều lần. Thay vào đó, chúng tôi sử dụng React event system và xử lý sự kiện click trực tiếp trên phần tử <button>
:
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')
);
Bạn có thể có nhiều component riêng biệt nhiều theo như ý bạn mong muốn, và sử dụng ReactDOM.render()
để render chúng trên những DOM container khác nhau. Dần dần, khi bạn chuyển nhiều các ứng dụng của bạn sang React, bạn sẽ có thể kết hợp nó thành những components lớn hơn, và sử dụng ReactDOM.render()
trong các hệ thống phân cấp.
Nhúng React vào một Backbone View
Backbone view đặc trưng sử dụng HTML string, hoặc các hàm string-producing template để tạo nội dung cho các phần tử DOM của nó. Quá trình này, cũng có thể được thay thế bởi việc render một component React.
Dưới đây, chúng ta sẽ tạo một Backbone view gọi là ParagraphView
. Nó sẽ ghi đè lên function render()
của Backbone để render một component <Paragraph>
vào phần tử DOM được cung cấp bởi Backbone (this.el
). Ở đây, chúng ta cũng sử dụng 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);
}
});
Điều này là quan trọng khi chúng ta cũng gọi ReactDOM.unmountComponentAtNode()
trong method remove
để React có thể xóa các hàm xử lý sự kiện và những tài nguyên khác liên quan tới component tree khi nó được gỡ.
Khi một component bị xóa từ bên trong một React tree, việc dọn dẹp được thực hiện một cách tự động, nhưng bởi vì chúng ta đang gỡ toàn bộ tree thủ công, chúng ta phải gọi method này.
Tích hợp với Model Layers
Trong khi bình thường chúng ta được đề xuất sử dụng data flow một chiều như React state, Flux, hoặc Redux, components React cũng có thể sử dụng một model layer từ những framework và thư viện khác.
Sử dụng Backbone Models trong Component React
Cách đơn giản nhất để sử dụng những Backbone model và collections từ một component React là lắng nghe các sự kiện thay đổi trước đó và ép buộc việc update một cách thủ công.
Những component chịu trách nhiệm việc render models sẽ lắng nghe những sự kiện 'change'
, trong khi những component chịu trách nhiệm việc render collections sẽ lắng nghe sự kiện 'add'
và 'remove'
. Trong cả hai trường hợp này, gọi this.forceUpdate()
để rerender component với data mới.
Trong ví dụ dưới đây, component List
render một Backbone collection, sử dụng component Item
để render những item bên trong.
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>
);
}
}
Chiết xuất data từ Backbone Models
Cách áp dụng trên yêu cầu những component React của bạn phải chú ý những model và collection Backbone. Nếu sau này bạn có kế hoạch chuyển sang một giải pháp quản lý data khác, bạn có thể muốn tổng hợp các kiến thức về Backbone một cách ít code nhất có thể.
Một giải pháp cho việc này là chiết xuất các attributes của model như là những data đơn giản, và giữ logic này trong một khu vực riêng biệt. Higher-order component chiết xuất tất cả attribute của một Backbone model vào một state, truyền data đến component con.
Bằng cách này, chỉ những higher-order component mới cần biết về bên trong Backbone model, và hầu hết các component trong app không đả động gì đến Backbone.
Ở trong ví dụ dưới đây, chúng ta sẽ tạo một bản copy của các attribute của model để hình thành một state khởi tạo. Chúng ta theo dõi sự kiện change
(và sẽ dừng lại khi unmount), và khi sự kiện xảy ra, chúng ta update state với những attribute hiện tại của model. Cuối cùng, chúng ta chắc chắn rằng nếu prop model
tự thay đổi, chúng ta sẽ không quên ngừng theo dõi model cũ, và chuyển qua theo dõi model mới.
Lưu ý rằng ví dụ này không có phải là đầy đủ trong việc làm việc với Backbone, nhưng nó sẽ cho bạn một ý tưởng về cách tiếp cận nó theo một cách chung chung:
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} />; }
}
}
Để chứng mình làm sao để sử dụng nó, chúng ta sẽ connect một component NameInput
đến một Backbone model, và update attribute firstName
của nó mỗi khi input thay đổi:
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')
);
Ký thuật này không chỉ giới hạn cho Backbone. Bạn có thể sử dụng React với bấy kỳ thư viện model nào bằng cách theo dõi các thay đổi của nó trong các method lifecycle và, tùy ý, copy các data vào state local React.