Can anyone tell me why this app is running into a "Function called outside component initialization" error? (Update: Found the cause of this specific error, but still have questions below about best practices for using rollup with svelte libraries.)
It seems to only be happening when I call getContext
(or onMount
, etc.) from a component (which should be allowed) inside of an {#each}
loop. But only happens if I include external: ['svelte']
in the library, so this may be as much a rollup question than a Svelte question.
Here's my code (which you can clone from here and try for yourself):
"dependencies": {
"my-new-component": "file:packages/my-new-component",
…
}
src/App.svelte
:
<script>
import { FieldArray } from "my-new-component";
import { UsesContext } from "my-new-component";
</script>
<FieldArray let:names>
{#each names as name, i}
<div>{name}: <UsesContext /></div>
{/each}
</FieldArray>
packages/my-new-component/src/FieldArray.svelte
:
<script>
let names = ['a']
const handleClick = () => {
names = ['a', 'b']
}
</script>
<button on:click={handleClick}>Blow up</button>
<slot names={names} />
packages/my-new-component/src/UsesContext.svelte
:
<script>
import {setContext, getContext} from 'svelte'
const key = {}
setContext(key, 'context')
let context = getContext(key)
</script>
{context}
Pretty basic stuff, right?
What am I doing wrong?
I understand that setContext
can only be called synchronously during component initialization (in the top level of the <script>
section) and that calling getContext
/setContext
or any lifecycle methods (onMount
) in an async way after the component has been initialized (such as from an event handler) can lead to (and is probably the most common cause of) this error.
But I am only calling it synchronously from the top-level script of the UsesContext.svelte
component ... so that can't be the problem, right?
The only thing that I am doing asynchronously is updating a let
variable. But that is one thing that one is allowed to do (and is commonly done) asynchronously with Svelte, isn't it?
(Of course, this is a contrived example to make it as minimal a reproducible example as possible. In the real library that I'm working on, I'm subscribing to form.registerField
from final-form, and updating the component's let
variables asynchronously from that callback ... an approach which is working just fine in the current version — but causes this error when I try to use it in the way described here.)
I don't feel like I'm doing anything that is not allowed in Svelte. Am I?
Things that cause the error to go away
If I change any one of the following factors (which should not make any difference), then everything works fine:
Take away the
{#each}
loop. (commit)<FieldArray let:names> <div>{names}</div> <UsesContext /> </FieldArray>
Update variable synchronously instead of asynchronously. (commit)
Copy the
UsesContext
component from the library into the app and import the local copy of the component instead. (commit)Even though it's an identical copy of the component, it works when imported from within the app but errors when imported from the library.
Use a local copy (commit) or "inline" version (commit) of the
FieldArray
component.Why doesn't it work when either of these is imported from a package? Might be related to the next factor...
Removing the
external: ['svelte']
frompackages/my-new-component/rollup.config.js
causes the error to go away. (commit)See "Should Svelte libraries use
external: ['svelte']
" below.
Why do any of those solve the problem? How are they all related?
Whose bug is it?
Is this a Svelte bug? It might be a bug related to initializing/detaching components within an {#each}
loop (since it only occurred for me with that combination)...
But I suspect the problem is more directly related to the way the libraries I'm using are packaging their code (with rollup). In particular, whether they do or don't include extra copies of Svelte's internal code.
Should Svelte libraries use external: ['svelte']
?
It is my understanding that when building a library, other libraries they depend on like React or Svelte should be listed both under both:
peerDependencies
external: [...]
so that a duplicate copy of React/Svelte/etc doesn't get installed under node_modules (in the case of peerDependencies
) or inline as part of the dist bundle that rollup builds (in the case of rollup's external
option). (See this article.)
It's probably a bigger deal to include an extra duplicate copy of a giant run-time library like React or Angular than it is to include an extra copy of the minimal run-time code used by Svelte. But it's not so much bundle size that I'm worried about as possible side effects/bugs that may result from having more than one copy of "Svelte" running around. (I've certainly run into problems like this before with React when I've had multiple instances of ReactDOM
floating around.)
So why isn't the official component-template
including external: ['svelte']
?
(And why did this comment suggest adding external: ['svelte/internal']
rather than external: ['svelte']
? Who imports directly from 'svelte/internal'
? Never mind, I think I discovered the answer to this part. More below.)
But why does (for example) svelte-urql use external
for all of its peerDependencies
/devDependencies
(including svelte
)? Should they not be doing that? Granted, in their case, they aren't currently including any svelte components (just helper functions and setContext
) yet, so that may be why it hasn't caused them any problems yet.
Ironically, I believe it was actually this "Function called outside component initialization" error that first prompted me to add this external: ['svelte']
line.
I had noticed in my app's bundle (built using webpack) that it included multiple copies of "svelte" — and by that, I mean multiple copies of the generic functions like setContext
. This concerned me, so I set out to try to figure out how to make it only include one copy of "svelte" in my bundle.
I was especially concerned when I saw multiple occurrences of let current_component;
/var current_component
in my app bundle.
In case you're wondering which libraries/modules these "copies" are coming from, it looks like it's these ones (comments kindly added by webpack):
!*** /home/…/svelte-final-form/dist/index.mjs ***!
(with noexternal: ['svelte']
)let current_component; function set_current_component(component) { current_component = component; } function get_current_component() { if (!current_component) throw new Error(`Function called outside component initialization`); return current_component; } function onMount(fn) { get_current_component().$$.on_mount.push(fn); } function onDestroy(fn) { get_current_component().$$.on_destroy.push(fn); } function setContext(key, context) { get_current_component().$$.context.set(key, context); }
!*** /home/…/my-new-component/dist/index.mjs ***!
(withexternal: ['svelte']
)let current_component; function set_current_component(component) { current_component = component; } const dirty_components = []; const binding_callbacks = []; …
(
function get_current_component()
didn't even appear in this section, apparently because the component's script referencesgetContext
from a different, external copy of Svelte, so rollup's tree-shaking noticed that its local version ofget_current_component()
was unused and it didn't need to include its definition:)function instance$1($$self) { console.log("my-new-component UsesContext"); const key = {}; Object(svelte__WEBPACK_IMPORTED_MODULE_0__["setContext"])(key, "context"); let context = Object(svelte__WEBPACK_IMPORTED_MODULE_0__["getContext"])(key); return [context]; }
!*** ./node_modules/svelte-forms-lib/build/index.mjs ***!
(with noexternal: ['svelte']
)var current_component; function set_current_component(component) { current_component = component; } function get_current_component() { if (!current_component) throw new Error("Function called outside component initialization"); return current_component; } function setContext(key, context) { get_current_component().$$.context.set(key, context); }
!*** ./node_modules/svelte-select/index.mjs ***!
(with noexternal: ['svelte']
)var current_component; function set_current_component(component) { current_component = component; } function get_current_component() { if (!current_component) throw new Error("Function called outside component initialization"); return current_component; } function beforeUpdate(fn) { get_current_component().$$.before_update.push(fn); }
!*** ./node_modules/svelte/internal/index.mjs ***!
(from[email protected]
)var current_component; function set_current_component(component) { current_component = component; } function get_current_component() { if (!current_component) throw new Error("Function called outside component initialization"); return current_component; } function beforeUpdate(fn) { get_current_component().$$.before_update.push(fn); } … function setContext(key, context) { get_current_component().$$.context.set(key, context); }
As you can see, each copy is a slightly different version of "svelte" (depending on the version number of svelte used to build each module, and on which unused functions got removed due to tree shaking).
My original hypothesis was that the if (!current_component) throw new Error("Function called outside component initialization");
error got hit because each component/library was maintaining their own copy of current_component
, so maybe when it crossed the boundary from one app's/library's component ("copy" of Svelte) to the other library's component ("copy" of Svelte), current_component
was undefined in that new scope even though it was correctly set in the old scope?
I still haven't ruled this out. And that hunch is what got me going trying to eradicate those extra "copies" by adding external: ['svelte']
in the first place — to try to resolve the error.
How external: ['svelte']
affects the contents of my-new-component
bundle
Here is how the output from my-new-component
changes when I add external: ['svelte']
:
⟫ git diff
diff --git a/dist/index.mjs b/dist/index.mjs
index a0dbbc7..01938f3 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,3 +1,5 @@
+import { setContext, getContext } from 'svelte';
+
function noop() { }
function assign(tar, src) {
// @ts-ignore
@@ -76,17 +78,6 @@ let current_component;
function set_current_component(component) {
current_component = component;
}
-function get_current_component() {
- if (!current_component)
- throw new Error(`Function called outside component initialization`);
- return current_component;
-}
-function setContext(key, context) {
- get_current_component().$$.context.set(key, context);
-}
-function getContext(key) {
- return get_current_component().$$.context.get(key);
-}
const dirty_components = [];
const binding_callbacks = [];
At first this looked like a really good thing, because it meant that this library could reuse the setContext
, getContext
functions (and presumably any other Svelte API functions) from its peer dependency — the svelte
package that is installed in the app's node_modules/
dir — rather than needlessly including a duplicate copy of those functions in the library's bundle.
But the more that I look into this, I wonder if that wasn't quite right. The most concerning thing is that even though some Svelte functions disappeared from my library's JS bundle, some of them — most notably set_current_component
and init
— remained in the bundle, because my library didn't specifically import
them — those are "internal" methods inserted by the Svelte compiler...
So maybe that is precisely the problem that caused the error: the init
/set_current_component
functions that remain in my library's bundle are referring to their own locally-scoped current_component
, but the getContext
/setContext
that I specifically imported end up calling get_current_component
from a different external copy of Svelte, which refers to a different current_component
in a different scope.
Oh, so that's why this comment suggest adding external: ['svelte/internal']
rather than external: ['svelte']
!
Update: Found solution to error (for this particular situation at least)!
When I tried adding 'svelte/internal'
to the external
list, a bunch of generic svelte functions disappeared from my library's bundle and got replaced with more Svelte import
s:
+import { SvelteComponent, init, safe_not_equal, text, insert, noop, detach, create_slot, update_slot, transition_in, transition_out } from 'svelte/internal';
import { setContext, getContext } from 'svelte';
-function noop() { }
-function assign(tar, src) {
…
-let current_component;
-function set_current_component(component) {
- current_component = component;
-}
The only lines that remain now are the generated functions (create_fragment
, create_fragment$1
, …) that are specific to the specific components. The bundle is super small now — 148 lines, down from 432. This is exactly what I was going for! Best of all, it causes the code to work (makes the error go away) (commit)
So I'm guessing the problem I ran into is because I only partially "externalized" svelte, so my library's bundle contained a mix of references to external copy of Svelte and internal copy of Svelte ... which couldn't see each other or share their copy of let current_component
with each other.
This error is particularly troublesome because it can be caused in various ways and the error doesn't reveal what the exact cause of the problem. So of course this fix only applies to this particular cause of the error.
I'm still not sure what caused me to get this error the first time (that prompted me to add external: ['svelte']
). It had to have been caused by something else before. I'm guessing I was doing something like trying to call getContext
from a callback that got triggered by final-form
asynchronously. If that happens again, at least I'll be better prepared and know how to solve it this time (probably move the getContext()
to the top of the script tag and use stores to handle the async calbacks).
Questions
To pull this all together, here are some high-level questions that I'd really like to understand:
Is Svelte an exception to the general principle of that "libraries that are expected to be used by both the app and one or more of its dependencies should be listed in those dependencies'
peerDependencies
andexternal
so that only one copy of those libraries ends up in the resulting app bundle? Or is that principle sound but am I just doing something wrong?Is it expected/okay for there be multiple copies of
current_component
/get_current_component()
in my app's .js bundle? Or should I be concerned to see this?If there are expected to be multiple copies of
current_component
(in an app containing components from multiple libraries), how do the various copies of "Svelte" coordinate between themselves? Or do they not need to because each component class is self-contained?I might be concerned, for example, that when a component passes off to the "next Svelte instance" (its children components, I presume), the
current_component
/parent_component
would be undefined here (but maybe that doesn't matter?):function init(component, options, instance, create_fragment, not_equal, props) { var dirty = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : [-1]; var parent_component = current_component; set_current_component(component); … set_current_component(parent_component); }
What if the different "copies" of Svelte are actually different versions of svelte package? Couldn't that cause errors if they interact with each other but have different APIs? (Or maybe the external APIs of the component class is stable so it doesn't matter if the internal API is different?)
- The nice thing about
peerDependencies
is that you only have one copy of each of them in your app. It just seems wrong to have multiple copies have a library in your app. But then I keep wondering if Svelte is an exception to that rule because it compiles components into self-contained classes (that can be used stand-alone or together) rather than needing a single run-time to tie them together into a single unified component tree like React? Does Svelte not need something like that, too, in order to handle context and stores that may cross library/component boundaries? How Svelte works is still too much of a mystery to me.
- The nice thing about
If there is a best practice for how a Svelte library should use
external
to avoid these sorts of potential problems? And if so, can we canonicalize it by including it in the component template? (I'll open an issue there.)It seems very strange to have to list both
'svelte/internal'
and'svelte'
. It seems likesvelte/internal
should be an implementation detail (of how svelte has organized its source tree internally) that consumers of svelte should not have to worry about. Why is this necessary and is there any way we could change svelte so that it's not necessary to list both?I've never seen any examples of other packages that require on odd suffix like
/internal
when adding toexternals
. All the examples you see (like in the docs) are just the main library name itself:external: ['some-externally-required-library'],
external: ['d3'],
Why is svelte an exception to that usual convention?
Not sure if it's related to Sapper but, I'm here because I encountered this issue when I moved
svelte
fromdevDependencies
intodependencies
in my Sapper app. The issue manifest as the SapperApp
component throwingtl;dr - keep
svelte
in devDependencies.I believe Sapper creates
svelte/internal
and having both Sapper's internal copy and the regular copy (now also present when callingNODE_ENV=production yarn install
) causes problems.Thanks for the detailed writeup - I never would have thought to look in
package.json
for this issue!