r3wt
r3wt

Reputation: 4742

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.

Upvotes: 2

Views: 502

Answers (2)

Patrick Roberts
Patrick Roberts

Reputation: 51916

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);

Upvotes: 1

dfsq
dfsq

Reputation: 193291

Your model decorator should not return new function. ctx will be passed to modelDecorator itself. So you really just need to return newly extended class from it:

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

Note, that the syntax you tried would work if your decorator was supposed to be used like this (Angular style decorators):

@model({ modelName: 'user' })
class Login extends PureComponent {}

Then you would need extra closure to keep passed parameters into your decorator:

function modelDecorator({ modelName }) {
  return (ctx) => {
    console.log('model name', modelName)
    return class extends ctx {
      constructor(...args) {
        super(...args);
        this.model = reactModelFactory(this);
      }
    }
  }
}

Upvotes: 1

Related Questions