(псевдо)Наследование для компонентов ReactJS

    Я хочу написать коротенький пост, про то, как я решил проблему наследования в ReactJS. Обычно, на форумах, люди советуют использовать миксины для наследования функционала, но, по-моему, это не совсем правильно. Все-таки трэйты/миксины и классы это не одно и то же, да еще и из-за возможности множественного наследования могут возникать вот такие казусы:

    var A = {
        doStuff (){}
    }
    
    var B = {
        doStuff (){}
    }
    
    var C = React.createClass({
        mixins: [A, B]
    });
    //упс... ошибка, потому что React не может решить какой из doStuff унаследовать
    



    К тому же, миксины не позволяют делать стандартные ООПешные фишки вроде перезаписи методов(method override):

    var A = {
        doStuff (){}
    }
    
    var C = React.createClass({
        mixins: [A],
        doStuff (){
             //неа, не получится
        }
    });
    


    А без этого, естественно, не сработает и расширение функционала как во «взрослых» ООП языках:

    doStuff (){
        super.doStuff()
        //дополнительный функционал
    }
    


    Конечно, классы ES6 решат эту проблему, и команда ReactJS к этому готовится, если судить по постам на их блоге, но ждать ES6 придется как второго пришествия, а затем, придется еще подождать пока не вымрут старые Интернет Эксплореры без поддержки ES6.
    Итак, я хочу предложить вам альтернативный метод, который и сам использую, но для этого вам понадобятся:
    1) Система модулей/зависимостей: RequireJS/Browserify/WebPack/что там еще сейчас в моде. Если вы не пользуетесь/не знаете что такое JavaScript модули, что ж, самое время узнать.
    2) Какая-нибудь функция/либа для глубокого копирования объектов, например, jQuery.extend, _.extend и т.п.

    Итак, я пишу модули своих компонент следующим образом:

    var React = require('react');
    var Human = {
        whoAreYou (){
             return "I'm a human";
        }
    
        whatDoYouDo (){
            return "I'm just chilling";
        }
    
        render (){
             return (
                  <h1>{this.whoAreYou()}<small>{this.whatDoYouDo()}</small></h1>
             )
        }
    }
    
    module.exports = {
         Class: Human,
         Component: React.createClass(Human)
    }
    


    Фишка в том, что я экспортирую не только компоненту, но и «чистый» объект, из которого эта компонента создается, таким образом, когда мне надо использовать просто компоненту <Human/>, я беру поле Component из экспорта моего модуля:

    var Human = require('human').Component;
    


    А вот когда мне надо от моего модуля унаследовать, и тут начинается самое интересное, я использую поле Class:

    var React = require('react');
    var Parent = require('human').Class;
    var Programmer = {};
    jQuery.extend(true, Programmer, Parent, {
        whoAreYou (){
             return Parent.whoAreYou.apply(this) + " and a programmer";//вызов метода из родителя!
        }
        
        whatDoYouDo (){
            //перезапись метода полностью
           return "I write code";
        }
    
        drinkCoffee (){
            //добавление нового метода
            console.log('*sip*');
        }
    });
    


    Естественно, этот модуль я тоже экспортирую согласно вышеописанной «конвенции»:

    module.exports = {
        Class: Programmer,
        Component: React.createClass(Programmer)
    }
    


    И теперь его можно использовать в приложении:

    var Programmer = require('programmer').Component;
    


    Ну, или наследовать/расширять дальше, например, в JuniorDeveloper.
    И на этом все, это был мой коротенький пост про костыль для (псевдо)наследования в ReactJS. Успешной вам трудовой недели, господа!

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 16

      +1
      А видели ли вы выступление Крокфорда на Nordiс.js? Он предлагает паттерн наследования, который очень похож на ваш.
        +2
        Не видел, но сейчас посмотрю. Я бы не называл это паттерном, мне кажется, это «костыль» до поры до времени, пока нам не завезут нормальные классы в ES6 или ES7.
        –1
        в dojo ребята предложили довольно не плохой вариант наследования на миксинах с блекджеком и куртизанками, и мне порой не хватает механизма в той же джабе
          0
          А есть ли у ребят из dojo какая статья по этой теме? С удовольствием бы взглянул.
          0
          Честно говоря, не понимаю, почему было не сделать возможность обращаться к методам микшин, как, например, в ExtJS, для решения проблемы с дубликатами имен? См. docs-origin.sencha.com/extjs/4.2.3/#!/api/Ext.Class-cfg-mixins
            0
            Таким способом нельзя написать обобщенный mixin, а только расширять классы
              0
              Поясните, что такое «обобщенный mixin»? Если можно, с примером.
                0
                Ну вот например:
                var Person = declare([], { say: function() { 
                 console.log("hello"); });
                var Man = declare([Person], { say: function() {
                 this.inherited(arguments);
                 console.log("i'm a man"); 
                });
                var Woman = declare([Person], { say: function() {
                 this.inherited(arguments);
                 console.log("i'm a woman"); 
                });
                var AwesomeMixin= declare([Person], { say: function() {
                 this.inherited(arguments);
                 console.log("And i'm awesome"); 
                });
                var AwesomeMan = declare([Man, AwesomeMixin]);
                var AwesomeWoman = declare([Woman, AwesomeMixin]);
                (new AwesomeMan()).say();
                (new AwesomeWoman()).say();
                

                я думаю понятно, что выдаст консоль
                  0
                  Признаюсь, без знания работы наследования в dojo, ответ был совсем не очевиден. Пришлось выполнять на jsfiddle. Не припомню случая, когда мне было необходимо именно такое поведение в повседневной работе.
          +4
          Я бы сделал так:
          module.exports = React.createClass(Human);
          module.exports.Class = Human;

          Теперь добраться до Human столь же просто, как и у вас — а вот до компонента добраться еще проще…
            +6
            В текущей версии createClass и createElement работают с Factory, но в последней альфа версии (npm install react@0.13.0-alpha.1) они уже работают с нормальными конструкторами и можно использовать наследование.

            //базовый класс компонента
            var BaseComponent = React.createClass({
              render: function() {
                throw new Error('Component must implement render method')
              }
            })
            
            //обычное прототипное наследование
            var MyComponent = function() {}
            MyComponent.defaultProps = {name: 'MyComponent'}
            MyComponent.prototype = Object.create(BaseComponent.prototype)
            
            MyComponent.prototype.render = function() {
              return React.DOM.div(null, this.hello())
            }
            MyComponent.prototype.hello = function() {
              return 'Hello, my name is ' + this.props.name
            }
            


            Точно так же можно использовать любой способ наследования, например, ES6 классы, которые по сути просто синтаксический сахар, или любую библиотеку для наследования.

            Отличие в том, что defaultProps нужно задавать статически.
            И ещё одно отличие в том, что для своих специальных методов реакт определяет порядок вызовов и может объединять их возвращаемые значения, когда они встречаются нескольких миксинах. А в обычном наследовании каждый переопределяющий метод должен сам вызывать базовый метод и что-то делать с его результатами, если ему это надо. Поэтому при таком наследовании нельзя использовать существующие миксины.
              0
              С нетерпением ждем релиза.
              0
              Сорри, я-таки не понимаю в чем проблема. Я использую все тот же Browserfy + transform(es6ify) кторый замечательно поддерживает классы и беды не знаю. + sourcemaps, в чем минус?
                0
                Просто JSX из grunt-react уже поддерживает некоторые фишки ES6(но далеко не все), поэтому не возникало повода использовать сторонний препроцессор. Но спасибо за наводку, обязательно попробую. Правда, я использую grunt, но думаю не составит труда нагуглить es6 трансформер под grunt.
                0
                Какой смысл экспортировать компоненту? Для использования класса не нужна компонента, она нужна только в момент рендера, а там можно с помошью фабрики создать компоненту. Так что тут нужен только один экспорт — класса.

                var IndexView = require('./templates/views/index'),
                    UsersView = require('./templates/views/users'),
                    UserView = require('./templates/views/user');
                
                var IndexFactory = React.createFactory(IndexView),
                    UsersFactory = React.createFactory(UsersView),
                    UserFactory = React.createFactory(UserView);
                
                //snip
                
                        this.route(urls.user, 'user', function (user_id) {
                            var user = users.get(user_id) || new User({'name': user_id});
                            React.render(UserFactory({model: user}), document.getElementById('content'));
                            user.fetch();
                            console.log('hello user')
                        });
                

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое