Современные JavaScript фреймворки, и ReactJS не исключение, обычно требуют эксклюзивного доступа к DOM и им очень не нравится, когда кто-то без их ведома этот DOM меняет. Проблема в том, что существует огромное количество сторонних библиотек (например, плагины jQuery), которым необходимо в их подконтрольном поддереве что-нибудь да вропнуть, анвропнуть, перенести в другое место и т.д. Обычно в таких случаях мы видим в консольке нечто подобное:
К счастью, эта проблема довольно легко и быстро решается. В этом посте я попробую изложить решение пошагово, но, если вам неинтересно, или вы спешите, просто поскрольте вниз к ссылке на гист с готовым решением. Итак, начнем.
Кто виноват?
Допустим, мы хотим использовать в нашем 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: "#/.configuren#make"
});
}, 1000);
setTimeout(function(){
Component.setProps({
contents: "#/.configuren#maken#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), пользуйтесь на здоровье.
Автор: diogene