"ReferenceError: <var name> is not defined" in production build while mixing "<script>" and "<script setup>"
Vue version
3.2.39
Link to minimal reproduction
https://github.com/M1CK431/demo-computed-issue
Steps to reproduce
yarn serve --mode=production
- Open http://localhost:8080/ in your browser
- Open the browser dev tools, console tab
- Try to change something in any form field
What is expected?
The underlying form data should be updated without any console error.
What is actually happening?
The underlying form data isn't updated and a console error appears:
ReferenceError: thing is not defined
System Info
System:
OS: Linux 5.19 Arch Linux
CPU: (8) x64 Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
Memory: 9.15 GB / 15.37 GB
Container: Yes
Shell: 5.1.16 - /bin/bash
Binaries:
Node: 18.9.0 - /usr/bin/node
Yarn: 1.22.19 - /usr/bin/yarn
npm: 8.19.2 - /usr/bin/npm
Browsers:
Firefox: 104.0.2
npmPackages:
vue: ^3.2.33 => 3.2.39
Any additional comments?
The issue occurred only in production build. Everything works as expected in development build.
The key here is the <script setup>
usage in App.vue
.
I have found two way to workaround this issue:
- replace the computed setter by a method (with exactly the same code)
- use only
<script>
to import components
Hi.
We recently saw a similar issue and our stance is this:
Your mixing of script and script setup is invalid. You are using it as a mere shortcut to get around registering components, while using Options API the define the actual component itself. That's not what script setup is designed to support.
Here's the explanation of what's going on: By defining a script setup block, that block defines the component. the additional script block is merely there to allow for a few options like inheritAttrs
to be set.
The reason your code breaks in production is that when compiling a script setup component for production, the render function compiled from the template is being inlined into the generated setup
function like this:
export default {
export default {
setup() {
const msg = ref()
return () => {
// render function code here
// can use msg, can't use name
}
},
data: () => ({
name: 'Jack'
})
}
It is compiled with the assumption that all template variables are within the closure of the render function, defined in (script) setup. It does not and - as a matter of complexity - cannot optionally take into account that variables might also have to be accessed through this
, so to speak.
Why does it work in dev? because in dev, in order to allow for better debugability, components are rather compiled like this:
export default {
setup() {
const msg = ref()
return {
msg,
}
},
render() {
// render function code here
// can use msg, and CAN use name
},
data: () => ({
name: 'Jack'
})
}
So what will happen with this issue?
- This pattern will not be supported in the future, so the fix is not in prod, but in dev:
- We need to better document this
- and detect it during dev so people don't falsely invest time in an unsupported pattern.
Thanks for the detailed explanation of what happens under the hood, it's heavily interesting! :ok_hand:
TBH, I was not having the feeling of doing something "wrong", even after carefully read the documentation. In fact, reading https://vuejs.org/guide/extras/composition-api-faq.html#can-i-use-both-apis-together, I was even convinced that mixing both API is something intentionally supported, and one of the biggest addition from Vue 3.
Now I figure out that I have to make a "per component" choice regarding the used API and my preference will go to option API in most cases, without a doubt. Let me explain why.
Personal background: I'm an experienced JS lead developer, and even before I was working as a Linux engineer (aka "sysadmin") in a small multinational, dealing with medium to big e-commerce platform. I was scripting with many languages, doing various tasks like automation, deployment, monitoring, etc... I also have some knowledge with network related things. Based on that experience, I guess I have a relatively large point of view about what is good tooling for development, maintainability, code spliting, responsibility, velocity, etc...
Framework comparison: I was working with AngluarJS, than React, and finally Vue 2. The last was the best IMHO. Why? Because, as the documentation stand, It allow [me] to "think less" when writing component code, letting me be more focus on build valuable features. And, at the end, it's the primary goal of a JS framework, isn't it?
Args exposed in documentation vs personal experience I read carefully the args here: https://vuejs.org/guide/extras/composition-api-faq.html#why-composition-api and I agree that there are a few use cases where Vue 2 was not as good as it could be and that composition API, by allowing a more fine grained control, is a good addition. Yet, that situations are excessively rare and so it's by for not enough to justify the overhead of using composition API everywhere instead of option API.
In addition, my experience let me think that most of the args lack of subtlety in their assertions. Let's take them one by one:
- Better Logic Reuse => this arg is built by comparison with mixins, but forget that there is others ways to extract parts of the logic outside a component and reuse them in multiple components or even in plain JS files. I'm personally heavily relying on bundler
import
/export
mechanism and I write most of the complicated things in plain, vanilla JS, located in regular.js
files. All of that in Vue 2. So, I don't really need a "better" way of reusing some logic. - More Flexible Code Organization => as explained in the previous point, it was not a problem at all with Vue 2. In the same part of the documentation, there is a side by side comparison between the same component written with both API, trying to prove that it looks better, less verbose, more elegant with composition API. That comparison make me laugh a lot! Let me say one thing: it's the second SPA webapp I'm migrating from Vue 2 to Vue 3 and I can promise that, in most cases, composition API is by far more verbose compare to options API, for the exact same result. The only real issue with the example component is that there is too many responsibilities in the same component, and the solution is simply to split it in many smaller components (and helpers functions) and the communication between all of them should be handled in a common reactive "context" object, shared using
import
/export
. At least it's the way we are handling heavily more complicated use cases in the webapps I lead. A word also about.value
everywhere (was even thinking about buying a "vue 3 dev kit" with spare kb keys.
v
a
l
u
e
... :smirk:), not very helpful to reduce verbosity... - Better Type Inference => never forget this single thing: JS stands on its own. At this time, no one can prove that something could be done using Typescript but not using Javascript. If people just stop thinking that JS automatic typing is a problem and finally open there eyes to discover that it's a feature (!!), also start to think just 5 minutes about there naming, then Typescript become totally useless. In fact, from a purely technical point of view, Typescript is counter productive because it needs an extra conversion step to be transformed as regular Javascript... At the end, providing typing as nothing more than a kind of bonus, but not at all something determinant to choose between one API or another.
- Smaller Production Bundle and Less Overhead => Honestly, is most Vue 2 webapp suffering about performance issue? Obviously not! So, again, it's a cool thing, but not all something determinant to choose between one API or another, especially nowadays where smartphones, tablets and computers are more powerful than ever before, and the same apply for network speed. In a general way, my approach about performance is: always keep it in mind to produce reasonably performant code (for ex:
filter
+map
=>reduce
), but really take care of it if (and only if) someone complain about it.
Sorry for the very long comment, I hope my point of view and experience sharing could be helpful for Vue dev team and community. I try to be as objective as possible, in the hope it could give some reasons to think about the benefits if Vue could intentionally support both API usage in the same component, and so we could have "best of both worlds" everywhere :heart_eyes:
I can understand your POV, and we left both APIs available precisely because many people prefer the Options API. However I don't really see how this relates to the issue being discussed here.
TBH, I was not having the feeling of doing something "wrong", even after carefully read the documentation.
Yep, as I said we need to be much clearer here.
However I don't really see how this relates to the issue being discussed here.
I'm explaining all of that to demonstrate why it could be a good feature to support both API usage in the same component:
Sorry for the very long comment, I hope my point of view and experience sharing could be helpful for Vue dev team and community. I try to be as objective as possible, in the hope it could give some reasons to think about the benefits if Vue could intentionally support both API usage in the same component, and so we could have "best of both worlds" everywhere :heart_eyes:
Yep, as I said we need to be much clearer here.
Anyway, thanks A LOT for deep explanation and clarification about the intention behind. Thanks to you, I have updated my component to stop using <script setup>
and now everything works as expected :+1:
So.. here we state that mixing both styles is an antipattern.
I agree tha using .value
is annoying but it is explicit when reading code. You have the ability to control the use of the reactive object or the value.
You have the ability to control the use of the reactive object or the value.
It's a good addition... when you need it. From my personal experience, most of the time, it's not needed. So go for options API most of the time, and use composition API for the rest :man_shrugging: