Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private scope for non-reactive data and methods #1988

Closed
ardoramor opened this issue Dec 8, 2015 · 48 comments
Closed

Private scope for non-reactive data and methods #1988

ardoramor opened this issue Dec 8, 2015 · 48 comments

Comments

@ardoramor
Copy link

Some data that is being kept track of by a component does not need to be made reactive. Currently, there is no one place to organize private data like that. Values stores in data, even if prefixed with underscore (e.g., _privateStuff), get methods added for every key of the object. I would like a dedicated space designated by Vue for private data and functions that will not be made reactive. I realize that I even now can create something like private or _private but there is no guarantee that it will not clash with Vue's future development.

It would be great to have a $private namespace allocated. It can be populated from private key of the component configuration object:

{
data: function () {...},
props: {},
methods: {},
private: {
// private variables that are not made reactive and are not watched
}
}

@yyx990803
Copy link
Member

For now the easy way to do it is just attach whatever you want in created hook, because when the hook is called Vue has already finished the observation phase:

{
  data: ...,
  created: function () {
    this.myPrivateStuff = {
      ...
    }
  }
}

Note all Vue api/internal properties start with either $ or _, so as long as your property doesn't use these two prefixes it will be totally fine.

@SuperSeb92
Copy link

Using the latest version (1.0.17), it seems properties created within the hook are still reactive after setting?

I'm doing this as a work-around:

this.privateThing = JSON.parse(JSON.stringify(this.privateThing));

@salivarick
Copy link

You can do this with created(those private props can't be track by vue), but I don't think it is reasonable that all props defined in vue data, props, methods are in one scope, and for vue 2.x if you define data with _ prefix, vue will tell you can't find this prop.

@emkman
Copy link

emkman commented Jun 7, 2017

I am still really unclear on how to do this/best practices in vue 2. I am loading the Stripe JS client inside a component and want it accessible anywhere within the component. It doesn't need to be available globally though that would be OK if that is the best option. The following code as well as numerous variations doesn't work:

mounted() {
    $script('https://js.stripe.com/v2/', function() {
      this.stripe = Stripe('pk_test_12345');
    });
},
stripe: {},

@emkman
Copy link

emkman commented Jun 7, 2017

I ended up getting it to work with

mounted() {
    const vue = this;
    $script('https://js.stripe.com/v3/', function() {
      vue.stripe = Stripe('pk_test_12345');
    });
},

Not sure if there is a better way though

@Glidias
Copy link

Glidias commented Aug 28, 2017

Yup, the basic principle is to set the property up later and not declare it in the initialized vue data itself. Do note that if such non-reactive data is "private' to begin with, why expose it to Vue? Why not just keep the data "privately" in the current file module you are working and rely on computed public getters to proxy into specific private data if your template requires those properties? (albeit, it might mean more boilerplate..). This is a question you've got to answer for yourself. After all, if such data is non-reactive, your template will not render reactively to those data anyway if your template references such data...which is something you must seriously consider beforehand especially if passing them as props to your components, since they aren't reactive anyway.

One approach i've been using so far (sort of like singleton/local services or lazily instatiated stuff..) is to resort to a computed property that will lazily instantiate/look-up something and store it in a nested private reference on the vue instance context if not yet found. For singletons/globals, you can simply return the value directly via a global service locator or DI-reference-container or something....

Example here: (note: ....not plain JS, but can be easily adopted in vanilla JS)
https://github.com/Glidias/haxevx/wiki/Integrating-non-reactive-data-services-class-instances,-etc.-into-Vue

@DeShadow
Copy link

DeShadow commented Sep 12, 2017

@Glidias Simple example for you.
I want to create Vue.js component for Cesium (or three.js, or d3.js etc.). That's why I need to initialize <div> inside component with Cesium.Viewer to have access to this instance later in my methods or events. I don't want to make this Viewer instance reactive. Because it's low-level object of 3rd-party library. That's why I want smth like this:

import Cesium from 'cesium';

export default {
    name: 'CesiumViewer',
    data(){ return { /* some data */ }; },
    created(){ this._cesium = new Cesium.Viewer(this.$el); },
    methods: {
        drawPath(points){ 
            /* some actions with this._cesium */
       }
    }
}

That's why I need a way to store internal variable of Cesium instance. And this instance is low-level and need no to be reactive.

@CrescentFresh
Copy link

My usecase is identical to @DeShadow, only for lunrjs. I initialize the lunr index object and use it throughout the lifetime of my component, however I don't need it reactive itself.

@prograhammer
Copy link

prograhammer commented Dec 27, 2017

Is there anything wrong with using this.$options to store non-reactive properties (as long as there is no future conflict with the options Vue adds globally such as name)? Looks cleaner than declaring them inside created ().

export default {
  name: 'MyDashboard',
  toolbar: 'My Dashboard',  // <-- non-reactive property

  mounted () {
    console.log(this.$options.toolbar)  // <-- use non-reactive property
  },

  data () {
    return {
      // reactive data
    }
  }
}

@LinusBorg
Copy link
Member

No, there's nothing wrong with that really. Many plugins use those as well, i.e. vue-i18n, vue-apollo, vuefire etc.

@PierBover
Copy link

I think the bigger problem here is the lack of a clear pattern.

Doing init in created() as proposed by @yyx990803 or using using $options as proposed by @prograhammer both do the job, but neither solution is explicitly saying "these are non reactive properties of this instance".

The logic in created() is related to the component lifecycle so it's obviously waiting for something to happen. That's not really what I have in mind when simply wanting to define some non reactive instance state. Also, there is no pattern here since everyone will call these properties whatever they want.

OTOH using $options seems hackish as it exposes Vue's guts but also there is no distinction between reactive and non reactive state either. Why should $options.myString live in the same level as $options.data or $options.methods?

A better solution IMO would be to have an explicit syntax to declare non reactive component state. Personally I really like the first idea @ardoramor had of using underscores for non reactive properties in data.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 8, 2018

Well, I can only repeat myself in saying that I don't feel that it is useful/important enough that it's worth adding a dedicated API for this.

but neither solution is explicitly saying "these are non reactive properties of this instance".

Anything in $options is not reactive. Isn't that enough? Not everything needs a dedicated API. React takes it to the extreme where everything is a prop or local state, and there's little to nothing else

OTOH using $options seems hackish as it exposes Vue's guts but also there is no distinction between reactive and non reactive state either. Why should $options.myString live in the same level as $options.data or $options.methods?

Again, nothing in $options is reactive. They are just the options used to create the component, and since non-reactive data, in my experience, is mostly exactly that - constants, options - I actually find the place quite fitting.

We are not React and want to provide abstractions where the use case justfies the added technical debt / maintaince burden. Personally, I just don't feel like this is the case here.

A better solution IMO would be to have an explicit syntax to declare non reactive component state. Personally I really like the first idea @ardoramor had of using underscores for non reactive properties in data.

That would be breaking change, so that's a definite "no".

@PierBover
Copy link

Again, nothing in $options is reactive.

Not being reactive is the natural state of things in JavaScript. Does that justify sticking non reactive component state anywhere that is non reactive?

They are just the options used to create the component, and since non-reactive data, in my experience, is mostly exactly that - constants, options - I actually find the place quite fitting.

It's quite contradictory that you'd argue for putting component state directly in $options.

Why does reactive component state live in $options.data (alongside $options.methods etc) instead of living directly in $options?

I can only assume that data means component state or reactive component state. In either case it's only logical that non reactive state would either go inside data or inside its own property (eg: staticData) and not directly in $options.

That would be breaking change, so that's a definite "no".

It makes sense. I'm only arguing that something need to be done here.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 8, 2018

It's quite contradictory that you'd argue for putting component state directly in $options.

The passage you quoted explicitly says that - in my experience - nonreactive "state" is usually just constant and options. In that sense it is not state at all, it won't change, it's just some constant you use in your code.

And if you talk about it being "directly" in $options, you can put it in a dedicated object, which again, is an established pattern many, many plugins and 3rd party components use to define config options, constants etc. pp. Also, we have flags like "functional:true", "inheritAttrs: false" that live "directly" in $options as well.

I don't doubt that there are instances where one has actual component state that can be mutated, but should not be reactive (e.g. for performance reasons), but that, in my experience, is an edge case not deserving its own API.

I'm only arguing that something need to be done here.

And I'm arguing that the issue you are seeing is not severe enough to warrant action that leads to an additional API.

Not everything needs its own API. Some things can be sufficiently solved by establishing a pattern. I personally think this is one of those cases. So we could add something in the docs about it to define some "official" pattern about how to deal with that,, but extending the API surface won't get my vote at least.

If we can agree on looking for an "offical" pattern, we should switch to the vuejs.org repo and open an issue about it there.

@PierBover
Copy link

PierBover commented Jan 8, 2018

In that sense it is not state at all, it won't change, it's just some constant you use in your code.

That's not what state means, but ok.

And if you talk about it being "directly" in $options, you can put it in a dedicated object

Yes, but that doesn't solve the problem of having a consistent and official pattern for solving this.

And I'm arguing that the issue you are seeing is not severe enough to warrant action that leads to an additionbal API.

Ok then.

@Akryum
Copy link
Member

Akryum commented Jan 8, 2018

The most 'official' way of declaring non-reactive state is:

created () {
  this.$_foo = 'bar'
}

@isometriq
Copy link

isometriq commented Feb 27, 2018

Given this base component...

export default {
  name: 'base',
  privates: {
    privateFirst() {
    },
    privateSecond() {
    }
  },
  methods: {
    publicFirst() {
    },
    publicSecond() {
    }
  }

Is it possible to extend/mixin a component with custom options (in my case privates) ?
Instead of explicitly like this...

export default {
  name: 'implementation',
  extends: Base,
  privates: Object.assign({
    privateSecond() {
      // override
    }
  }, Base.privates)
}

@stowball
Copy link

stowball commented Apr 6, 2018

I too would really like an explicit place (that isn't manually done on created() to store non-reactive data. I use them all the time (for things like "dictionaries") that are used in the rendered templates (but never need to trigger a re-render).

I'm considering doing this:

Vue.mixin({
  created() {
    if (this.$options.statics) {
      Object.keys(this.$options.statics).forEach((staticVar) => {
        this[staticVar] = this.$options.statics[staticVar];
      });
    }
  },
});

and then in my component definitions I can do:

{
  props: {},
  statics: {
    foo: 'bar'
    baz: 'qux',
  },
  data() {
    return {},
  }
}

@matheusgrieger
Copy link

If using $options to declare static data (such as Vuetify's data-table headers, that are used in templates but do not change and don't need to be reactive), Vue's TypeScript declarations should be changed to account for theses cases, as adding any additional properties to a component declaration triggers an "Unkown property" error. I would try to do that, but I don't know exactly how theses declarations work yet...

@LinusBorg
Copy link
Member

It's actually possible to do that, Ams documented here:

https://vuejs.org/v2/guide/typescript.html#Augmenting-Types-for-Use-with-Plugins

@samuelantonioli
Copy link

samuelantonioli commented Jul 6, 2018

I don't see a big problem here.

// in utils/no-react.js
import Vue from 'vue'
function VueNoReact(obj) {
    // https://github.com/rpkilby/vue-nonreactive
    var Observer = (new Vue()).$data.__ob__.constructor;
    obj.__ob__ = new Observer({});
    return obj;
}
export default VueNoReact

Usage:

<template>...</template>
<script>
import VueNoReact from '@/utils/no-react' 
export default {
    data() {
        return {
            non_tracked_variable: null,
        };
    },
    mounted() {
        this.non_tracked_variable = VueNoReact(google_map_or_something_else);
    },
};
</script>

I think this functionality can be easily implemented using the function above. I don't think it's necessary to include it into the core of Vue.


I'm against $options for non-reactive state. It's still component data and it should therefore live where all the other data of the component lives. People use $options just because they don't know about ways to add non-reactive objects to vue_instance.data. The way I describe here (thanks to rpkilby) allows us to keep the data where it should be.

The good thing about this way is that you can't add reactivity to non-reactive objects by accident.

@LinusBorg
Copy link
Member

While this is a clever hack, it's a hack - you're using an internal API that coudl break anytime, so I won't recommend this as a nice workaround ...

@samuelantonioli
Copy link

If @yyx990803 could add this as a official Vue plugin, it would be ok IMHO. As soon as the core changes, the plugin gets an update, too. This is the easiest solution with least effort for him.

Could you imagine to use this way when it gets supported officially (and is not considered a hack anymore)?

@LinusBorg
Copy link
Member

I probably don't think so. We are already working on the 2.*-next experimental branch to the official branch, which will use ES6 Proxies to implement Reactivity. This hack, official or not, would not work with that branch since there won't be any __ob__ property, for example.

@samuelantonioli
Copy link

samuelantonioli commented Jul 6, 2018

I think it's clear that we have to find a solution for non-reactive data.

  1. Adding it to the core could lead to common practices that aren't helpful for beginners (and lead to big efforts for relatively small gains for the core developers).

  2. Adding it as a plugin tells framework users that it's still relatively uncommon but shows a best practice ("use this plugin" instead of "just add it to $options although it doesn't belong there").

The psychological implications of framework development are definitely to consider when changing the approach. When everyone thinks that $options is not meant for non-reactive data but even core developers advocate for it, it will lead to the impression that the framework has inconsistencies in the planning stage.


When something like

export default {
    static() {
        // like data(), but for non-reactive properties
        return {
            test: null,
        };
    },
    mounted() {
        // I can directly use it
        this.test = {};
    },
};

gets added, it can definitely work as a best practice. But I'm not a core developer for Vue, so I would like to give solutions that are of least effort for the core developers (like a plugin solution).

Open question for static: Overshadowing of variables (affects data, props and static).

@chriswait
Copy link

@LinusBorg Should developers assume that the structure/properties of $options will be relatively stable in future releases? I was assuming it was a Vue.js internal and probably not to be depended on, partially because I couldn't find any documentation/recent examples of how it should be used.

I might feel more secure with this weaker-than-API / stronger-than-convention approach if there was a dedicated property i.e $options.foo that was reserved for this, documented somewhere, added to Vue's TypeScript declarations etc.

@samuelantonioli
Copy link

samuelantonioli commented Jul 6, 2018

I wouldn't like to use something like $options.foo because it causes friction in usage (e.g. in templates or elsewhere where you have to do $options.foo.somevar every time). As I said, reusing other parts of the core framework to add functionality in retrospect would be considered as a hack for many (and doesn't build trust in the community). I think the solution would be ok, but still feels hack-ish.

@LinusBorg
Copy link
Member

LinusBorg commented Jul 6, 2018

Should developers assume that the structure/properties of $options will be relatively stable in future releases? I was assuming it was a Vue.js internal and probably not to be depended on, partially because I couldn't find any documentation/recent examples of how it should be used.

$options is part of the public API, and documented in our API docs:

The instantiation options used for the current Vue instance. This is useful when you want to include custom properties in the options:

new Vue({
  customOption: 'foo',
  created: function () {
    console.log(this.$options.customOption) // => 'foo'
  }
})

This is the default approach that countless plugins are using to have developers define per-instance options for their plugins, like vue-i18n, vue-apollo, vuelidate etc. pp.

@LinusBorg
Copy link
Member

LinusBorg commented Jul 6, 2018

@samuelantonioli You have this example of a desired official API:

export default {
    static() {
        // like data(), but for non-reactive properties
        return {
            test: null,
        };
    },
    mounted() {
        // I can directly use it
        this.test = {};
    },
};

Here's aminimal implementation:

Vue.mixin({
  beforeCreate() {
    const static = this.$options.static
    if (static && typeof static === 'function') {
      staticData = static()
      if (typeof staticData === 'object') {
        Object.assign(this, staticData)
      }
    }
  }
})

An "official" implementatio would use $options just the same.

@samuelantonioli
Copy link

So is it best practice that we use something like this?

<template>
    <div>
        Test: {{$options.static.test}}
    </div>
</template>
<script>
export default {
    static: {
        test: null,
    },
    mounted() {
        this.$options.static.test = google_map_or_something_else;
    },
};
</script>

If yes, I would love to see it in the guides or the docs so framework users know what the best practice is (which leads to trust and a supported way).

@LinusBorg
Copy link
Member

LinusBorg commented Jul 6, 2018

So is it best practice that we use something like this?

As mentioned by Evan right at the beginning of this duscussion, people usually just add those properties in created

@samuelantonioli
Copy link

Ok, looks good. It seems that it wouldn't be too hard to support non-reactive data in the Vue core. Do you plan to add it to the core (e.g. called static)?

Otherwise I would write a plugin that injects a global mixin which adds static support.

I don't have a problem with any solution (even using created, although it's not as good-looking as a dedicated variable IMO), but I would like to be able to tell people I teach Vue to what the best practice is.

@samuelantonioli
Copy link

samuelantonioli commented Jul 6, 2018

So basically:

// your idea
// as a plugin

// in plugins/static.js
function install(Vue) {
    Vue.mixin({
        beforeCreate() {
            const static = this.$options.static;
            if (static && typeof(static) === 'function') {
                Object.assign(this, static.apply(this));
            } else if (static && typeof(static) === 'object') {
                Object.assign(this, static);
            }
        },
    });
}

export default install;

And done. I think this could be perfectly added to a guide. Would you approve this as a best practice?

@samuelantonioli
Copy link

samuelantonioli commented Jul 6, 2018

I've created a plugin and published it on npm which adds support for static.

See Vue-Static or the npm package. Now I can use this as a best practice for my employees.

Thanks very much, @LinusBorg !


So for the use case of @CrescentFresh and @DeShadow

<script>
import Cesium from 'cesium';

export default {
    name: 'CesiumViewer',
    data() {
        return {
            /* some data */
        };
    },
    static() {
        return {
            _cesium: new Cesium.Viewer(this.$el),
        };
    },
    methods: {
        drawPath(points){ 
            /* some actions with this._cesium */
       },
    },
};
</script>

I think this is a clear and easy-to-learn pattern.

@backbone87
Copy link

The approaches listed here dont solve the problem when using typescript without vue-class-component.

I want to store a timeout handle in the component instance. But just assigning a variable in beforeCreated/created hook doesnt work, because typescript thinks it isnt declared.

Having a static option that is like data but for non reactive properties in the core and appropriate generic typing would make non-class-style components much easier to deal with in typescript.

One could say: "Just use class style components", but while looking pretty sweet, in the background a lot of hackyish stuff gets done. I wonder if there is a plan to fully support class based components without reflecting over the class? Something like this:

class MyComp extends Vue {
  constructor() {
    super({ /* component options not expressable via class constructs */ });
  }
}

new MyComp().$mount('#app');

@kghost
Copy link

kghost commented Dec 5, 2018

Here is a helper function to create a non-reactive object

function NonReactive<T>(obj: T) {
  return (new Proxy(obj as any, {
    get(oTarget, sKey) {
      return oTarget[sKey];
    },
    set(oTarget, sKey, vValue, receiver) {
      oTarget[sKey] = vValue;
      return true;
    },
    defineProperty(oTarget, sKey, oDesc) {
      return true;
    },
  }) as any) as T;
}

use it like this

$store.volatile = NonReactive({
  state: 0,
});

Then you can edit volatile.state freely.

$store.volatile.state = 100

@Bellski
Copy link

Bellski commented Dec 6, 2018

Is it possible to make nested property to be non reactive ? even with dirty hack ?

sdreher added a commit to ccnmtl/ahemap that referenced this issue Mar 6, 2019
Initializing non-reactive data in the created method. Reference -- this rather long-ish thread about Vue.js scope vuejs/vue#1988
@shaunc
Copy link

shaunc commented Mar 10, 2019

So ... what is the official way to create non-reactive internal data in typescript with @Component?

  • $options are not private, so can't be used for internal state
  • in created() { ... } doesn't allow you to declare type
  • Component({ static: { ... }}) -- also doesn't allow type declaration -- if you access this._foo, how to get it type checked?

UPDATE Here's a thought: doc says "undefined will not be reactive" -- so perhaps we can do:

@Component
export default class Foo extends Vue {
  private _internal: string = undefined as any
  created () {
    this._internal = 'foo'
  }
  myMethod () {
    if (this._internal === 'foo') { ... } // type checked correctly
    ...
  }
}

Will that work?

@Colorfulstan
Copy link

Colorfulstan commented Mar 27, 2019

@shaunc that would work.
Here is how I recently started typing my $options to store constant data, which is the same approach you're thinking about:

@Component
export default class Foo extends Vue {
  $options!: ComponentOptions<Vue> & {
    internal: string
  }

  beforeCreate() {
    this.$options.internal = 'foo'
  }
  myMethod () {
    if (this.$options.internal === 'foo') { ... } // type checked correctly
    ...
  }
}

@kispi
Copy link

kispi commented Sep 5, 2019

Why would someone need this? Can being reactive value cause what problem?

@matheusgrieger
Copy link

@kispi at the moment, creating reactive data requires some processing and increases memory consumption. So if you have, for instance, a large array of objects, hard-coded, that has to be displayed on your app, Vue would iterate over each of the objects and create reactive properties even though they would never change.

This is expected to be less of an issue with the upcoming release of Vue 3, though, as it'll use Proxies instead of the current Getter/Setter observable.

@kispi
Copy link

kispi commented Sep 6, 2019

@matheusgrieger
Ah, I think I was using just another plain old javascript file for those non-reactive data and import them from Vue instance, not via store or any Vue-cared feature. In this case, I had to even use $set to add reactivity if needed.

Never realized that someone might have needed this since I've never needed it.

For the case you mentioned, I think I saw the post that he removed reactivity from the large dataset by using Object.freeze inside the mutation.

@AThiyagarajan
Copy link

So ... what is the official way to create non-reactive internal data in typescript with @Component?

  • $options are not private, so can't be used for internal state
  • in created() { ... } doesn't allow you to declare type
  • Component({ static: { ... }}) -- also doesn't allow type declaration -- if you access this._foo, how to get it type checked?

UPDATE Here's a thought: doc says "undefined will not be reactive" -- so perhaps we can do:

@Component
export default class Foo extends Vue {
  private _internal: string = undefined as any
  created () {
    this._internal = 'foo'
  }
  myMethod () {
    if (this._internal === 'foo') { ... } // type checked correctly
    ...
  }
}

Will that work?

With the typescript:

private _internal: string = undefined as any could be simply written as private _internal!: string; without specifying the value and with a "!" after the variable name.

And still the initial value of this._internal will be printed in the html as 'foo'.
But further changes to the this._internal will not be updated in the view/html.

@ddenev
Copy link

ddenev commented Jun 28, 2020

Wouldn't this be a good approach for typescript?

import Vue from 'vue'

declare module 'vue/types/vue' {
  interface Vue {
    myVariable: MyType
  }
}

export default Vue.extend({
  ...
  created () {
    Object.defineProperty(this, 'myVariable', { writable: true })
  },
  methods: {
    myMethod () {
      // use this.myVariable
    }
  }
})

@ddenev
Copy link

ddenev commented Jun 28, 2020

And yet another typescript approach could be (original idea - https://stackoverflow.com/a/60072530/7023723):

interface NonReactive {
  myVariable: MyType
}

export default Vue.extend({
  ...
  data () {
    return {
      nonReactive: {} as NonReactive
    }
  },
  created () {
    // initialize if needed
    this.nonReactive.myVariable = new MyType()
  },
  methods: {
    myMethod () {
      // use this.nonReactive.myVariable
    }
  }
})

This approach utilizes the fact that properties that are not initially described in data() will not be made reactive when added later.

@cmcleese
Copy link

cmcleese commented Feb 4, 2022

Any Vue 3 composition API suggestions?

@SunboX
Copy link

SunboX commented May 12, 2022

I also came across this looking for a Vue3 TS Composition API suggestion.

@AndreKR
Copy link

AndreKR commented Mar 29, 2023

Looks like the old underscores are back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests