Table of contents


Imports are not values
Imports in JavaScript are not static values, but references that are dynamic.
They are called live bindings.
Understanding live bindings
Suppose you have a module that exports a variable like this:
let foo = 'initialValue';
export { foo };
You import it like this in another module:
import { foo } from './foo.js';
console.log(foo);
This will print "initialValue"
as expected.
Now, if we change foo
after exporting it:
let foo = 'initialValue';
export { foo };
foo = 'changedValue';
This prints "changedValue"
.
This means the imported foo
is not just a static copy, but it’s a reference to the original variable in the exporting module.
It seems obvious, and well, it kind of is. But things get interesting with default exports.
Default exports
When we switch the same code to use default exports:
let foo = 'initialValue';
export default foo;
foo = 'changedValue';
import foo from './foo.js';
console.log(foo);
It prints "initialValue"
even though the original variable was modified to "changedValue"
. Why does this happen?
Well, default exports treat the exported value as an expression, not a binding.
This is a feature of the default exports spec, and not a bug, because it allows us to do things like:
export default 'hello!';
Or this:
export default 1 + 2;
But it also means that we lose the live connection to the original variable.
Things get weird with function and class declarations, though.
Weird case
If we do:
export default function foo() { console.log('hello, world!');}
foo = 'changedValue';
In this case, the default import prints "changedValue"
. How?
It turns out that function and class declarations are an exception to the default exports’ “exported as expression” rule, and they’re treated as live bindings again.
This does not really make sense to me.
If the browser can detect that the exported value is a function or class declaration and treat it as a live binding, then exported variables such as export default variable
should also be bindings.
But it doesn’t work this way.
There is a workaround, though, to make default exports live.
Workaround
By using the named export syntax:
let foo = 'initialValue';
export { foo as default };
foo = 'changedValue';
This works as expected and prints "changedValue"
.
In my opinion, this is not really intuitive. Default exports don’t always behave like normal exports.
That’s why I am going to stop using default exports as much as possible, and I think you should, too!
← Back to blog