Search code examples
javascriptreactjsdecoratorecmascript-next

How to create a decorator that adds a single function to the component class?


I should preface this by saying I understand very little about es7 decorators. Basically what I want is a decorator called @model that adds a function to a component called model. So for example I'd call it like

@model class FooBar extends Component { }

and then the FooBar class would now have the model function.

Here's what I tried:

Model.js

export default function reactModelFactory( ctx ){

    return (key)=>{
        return {
            onChange: (e)=>ctx.setState({[key]:e.target.value}),
            value: ctx.state[key],
            name: key
        };
    };

};

function modelDecorator() {
    return function(ctx){
        return class extends ctx{
            constructor(...args){
                super(...args);
                this.model = reactModelFactory(this);
            }
        }
    }
}

export { modelDecorator as model };

Login.js

import React,{PureComponent} from 'react';
import {model} from './Model';

@model class Login extends PureComponent{}

React throws with error message:

TypeError: Super expression must either be null or a function, not object

I have no idea what this means. I'm looking for some help in getting my decorator working, and a bonus would be to understand the concept of decorators at all.


Solution

  • To add to @dfsq's answer (I'm assuming it does what you want), you can go a step further in terms of interface performance by adding model() to the prototype instead of to each instance like this:

    export default function reactModelFactory() {
      return function model (key) {
        return {
          onChange: (e) => this.setState({ [key]: e.target.value }),
          value: this.state[key],
          name: key
        };
      };
    };
    
    function modelDecorator(Class) {
      Object.defineProperty(Class.prototype, 'model', {
        value: reactModelFactory(),
        configurable: true,
        writable: true
      });
    
      return Class;
    }
    

    This is much better for performance as it causes the decorator to modify the existing class's prototype a single time with the model member method, rather than attaching a scoped copy of the model method within the anonymous extended class's constructor each time a new instance is constructed.

    To clarify, this means that in @dfsq's answer, reactModelFactory() is invoked each time a new instance is constructed, while in this answer, reactModelFactory() is only invoked a single time when the decorator is activated on the class.

    The reason I used configurable and writable in the property descriptor is because that's how the class { } syntax natively defines member methods on the prototype:

    class Dummy {
      dummy () {}
    }
    
    let {
      configurable,
      writable,
      enumerable
    } = Object.getOwnPropertyDescriptor(Dummy.prototype, 'dummy');
    
    console.log('configurable', configurable);
    console.log('enumerable', enumerable);
    console.log('writable', writable);