DOM-а чи вистачить на всіх, або як помирити ReactJS з тим фактом, що сторонні бібліотеки змінюють його DOM

Сучасні JavaScript фреймворки, і ReactJS не виняток, зазвичай вимагають ексклюзивного доступу до DOM і їм дуже не подобається, коли хтось без їх відома цей DOM змінює. Проблема в тому, що існує величезна кількість сторонніх бібліотек (наприклад, jQuery плагіни), яким необхідно їх підконтрольному поддереве що-небудь та вропнуть, анвропнуть, перенести в інше місце і т. д. Зазвичай в таких випадках ми бачимо в консольке щось подібне:

image

На щастя, ця проблема досить легко і швидко вирішується. У цьому пості я спробую викласти рішення покроково, але, якщо вам нецікаво, або ви поспішаєте, просто поскрольте вниз до посиланні на гіст з готовим рішенням. Отже, почнемо.

Хто винен?
Припустимо, ми хочемо використовувати в нашому ReactJS проекті якийсь мега-крутий редактор тексту, за типом AceEditor або TinyMCE. Цей плагін бере елемент <textarea/> і перетворює його в <div contenteditable/>, з тулбаром і хайлатом, і він може виглядати, наприклад, так:

function textarea2editor(parent){
var $parent= jQuery(parent);
var $editor = jQuery('<div contenteditable/>');
$editor.css('background', "#333");
$editor.css("color", "#efefef");
$parent.find('textarea').replaceWith($editor);
/*...*/
return {
setText: function (text){
$editor.html(text);
},
/*...*/
} 
}


Припустимо, у нас є ReactJS додаток, яке виводить Unix команди з заданим інтервалом:

var App = React.createClass({
render: function() {
return (
<div>
<textarea value={this.props.contents}/>
</div>
)
}
});

var Component = React.render(<App contents="#./configure" />, document.body);
setTimeout(function(){
Component.setProps({
contents: "#/.configure\n#make"
});
}, 1000);
setTimeout(function(){
Component.setProps({
contents: "#/.configure\n#make\n#make install"
});
}, 2000);


Нам би хотілося, звичайно ж, використовувати вищеописаний плагін для хайлайта команд, для цього, як зазвичай, ми спочатку додамо в нашу компоненту метод componentDidUpdate, який займеться ініціалізацією редактора:

componentDidMount: function(){
this.editor = textarea2editor(this.getDOMNode());
this.editor.setText(this.props.contents);
}


А також метод componendDidUpdate, який буде оновлювати вміст <div contenteditable/> при кожному оновленні компоненти:

componentDidUpdate: function(){
this.editor.setText(this.props.contents);
}


І ось у нас вийшов наступний фиддл.
Коли ж ми його запустимо, ми побачимо, що в «терміналі» з'являється лише перша команда, але не інші. І якщо ми відкриємо консольку, то побачимо, чому це відбувається:



Справа, звичайно ж, в тому, що наш редактор замінив <textarea/> <div contenteditable/> без відома React, і тепер React в замішанні, у нього в віртуальному будинку є <textarea/>, а в реальному будинку немає, і незрозуміло, як в таких умовах робити діфф і оновлювати сторінку.

І що робити?
На щастя, рішення дуже просте, але в голову воно мені прийшло не відразу. Надихнув мене на це рішення досвід спілкування з AngularJS, де є директива ngNonBindable, яка як би говорить Ангуляру:



Я задумався, а чи немає в React-е чого-небудь подібного. В документації про це (прямо) не сказано, але сказано про метод shouldComponentUpdate, який повертає логічне значення, і якщо воно хибне, то React не стане оновлювати не тільки функції, але і всі її піддерево. Тобто, він просто не стане викликати методи componentWillUpdate, componentWillReceiveProps, render і т. д. Цей метод пропонується в якості засобу оптимізації, але зачекайте-но, а якщо він не викличе render, то піддерево компоненти у віртуальному DOM не зміниться, значить діфф для цього віртуального піддерева і відповідного йому реальному DOM в принципі не потрібен, чи означає це, що за допомогою цього «оптимізаційного методу, можна змусити Реактив ігнорувати певні частина підвладному йому DOM-a? Виявляється, можна, але якщо ми додамо в нашу компоненту:

shouldComponentUpdate: function (){
return false;
}

помилки-то зникнуть, але наш «термінал» все одно не буде оновлюватися. Насправді, нам потрібно змусити React ігнорувати тільки <textarea/>, а не всю компоненту, невже для цього нам доведеться писати RenderOnceTextarea і так кожного разу, коли ми хочемо використовувати компоненту з React.DOM?

Насправді, є рішення краще — написати компоненту ReactIgnore, яка завжди повертає своїх дітей, і завжди повертає помилкове в shouldComponentUpdate:

var ReactIgnore = React.createClass({
displayName: 'ReactIgnore',
shouldComponentUpdate: function(){
return false;
},

render: function (){
return React.Children.only(this.props.children);
}
});


Ось працюючий фиддл.

tl;dr aka посилання на гіст
Я скопіпастив ReactIgnore з свого проекту і виклав в якості гіста (ахтунг, ES6 Harmony), користуйтеся на здоров'я.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.