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

в 8:40, , рубрики: javascript, ReactJS, reactjs jquery 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: "#/.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);
    }

И вот у нас получился следующий фиддл.
Когда же мы его запустим, мы увидим, что в «терминале» появляется только первая команда, но не остальные. И если мы откроем консольку, то увидим, почему это происходит:

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

Дело, конечно же, в том, что наш «редактор» заменил <textarea/> на <div contenteditable/> без ведома React, и теперь React в замешательстве, у него в виртуальном доме есть <textarea/>, а в реальном доме нет, и непонятно, как в таких условиях делать дифф и обновлять страницу.

И что делать?

К счастью, решение очень простое, но в голову оно мне пришло не сразу. Вдохновил меня на это решение опыт общения с AngularJS, где есть директива ngNonBindable, которая как бы говорит Ангуляру:

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

Я задумался, а нет ли в 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js