`);
// test 3: string reactive property set
- expect(stripExpressionMarkers($('my-element').html())).to.include(
+ expect(stripExpressionMarkers($('#default').html())).to.include(
`
initialized
`
);
// test 4: boolean reactive property correctly set
// Lit will equate to true because it uses
// this.hasAttribute to determine its value
- expect(stripExpressionMarkers($('my-element').html())).to.include(`
`);
// test 5: object reactive property set
// by default objects will be stringified to [object Object]
- expect(stripExpressionMarkers($('my-element').html())).to.include(
+ expect(stripExpressionMarkers($('#default').html())).to.include(
`
data: 1
`
);
// test 6: reactive properties are not rendered as attributes
- expect($('my-element').attr('obj')).to.equal(undefined);
- expect($('my-element').attr('bool')).to.equal(undefined);
- expect($('my-element').attr('str')).to.equal(undefined);
+ expect($('#default').attr('obj')).to.equal(undefined);
+ expect($('#default').attr('bool')).to.equal(undefined);
+ expect($('#default').attr('str')).to.equal(undefined);
// test 7: reflected reactive props are rendered as attributes
- expect($('my-element').attr('reflectedbool')).to.equal('');
- expect($('my-element').attr('reflected-str')).to.equal('default reflected string');
- expect($('my-element').attr('reflected-str-prop')).to.equal('initialized reflected');
+ expect($('#default').attr('reflectedbool')).to.equal('');
+ expect($('#default').attr('reflected-str')).to.equal('default reflected string');
+ expect($('#default').attr('reflected-str-prop')).to.equal('initialized reflected');
+ });
+
+ it('Sets defer-hydration on element only when necessary', async () => {
+ // @lit-labs/ssr/ requires Node 13.9 or higher
+ if (NODE_VERSION < 13.9) {
+ return;
+ }
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ // test 1: reflected reactive props are rendered as attributes
+ expect($('#non-deferred').attr('count')).to.equal('10');
+
+ // test 2: non-reactive props are set as attributes
+ expect($('#non-deferred').attr('foo')).to.equal('bar');
+
+ // test 3: components with only reflected reactive props set are not
+ // deferred because their state can be completely serialized via attributes
+ expect($('#non-deferred').attr('defer-hydration')).to.equal(undefined);
+
+ // test 4: components with non-reflected reactive props set are deferred because
+ // their state needs to be synced with the server on the client.
+ expect($('#default').attr('defer-hydration')).to.equal('');
});
it('Correctly passes child slots', async () => {
@@ -74,7 +97,7 @@ describe('LitElement test', function () {
const $slottedMyElement = $('#slotted');
const $slottedSlottedMyElement = $('#slotted-slotted');
- expect($('my-element').length).to.equal(3);
+ expect($('#default').length).to.equal(3);
// Root my-element
expect($rootMyElement.children('.default').length).to.equal(2);
diff --git a/packages/integrations/lit/package.json b/packages/integrations/lit/package.json
index ac0a8608c380..1136570ee06a 100644
--- a/packages/integrations/lit/package.json
+++ b/packages/integrations/lit/package.json
@@ -23,6 +23,7 @@
".": "./dist/index.js",
"./server.js": "./server.js",
"./client-shim.js": "./client-shim.js",
+ "./dist/client.js": "./dist/client.js",
"./hydration-support.js": "./hydration-support.js",
"./package.json": "./package.json"
},
diff --git a/packages/integrations/lit/server.js b/packages/integrations/lit/server.js
index 83737c183a1f..48a3c646ffa8 100644
--- a/packages/integrations/lit/server.js
+++ b/packages/integrations/lit/server.js
@@ -36,10 +36,18 @@ function* render(Component, attrs, slots) {
// LitElementRenderer creates a new element instance, so copy over.
const Ctr = getCustomElementConstructor(tagName);
+ let shouldDeferHydration = false;
+
if (attrs) {
for (let [name, value] of Object.entries(attrs)) {
- // check if this is a reactive property
- if (name in Ctr.prototype) {
+ const isReactiveProperty = name in Ctr.prototype;
+ const isReflectedReactiveProperty = Ctr.elementProperties.get(name)?.reflect;
+
+ // Only defer hydration if we are setting a reactive property that cannot
+ // be reflected / serialized as a property.
+ shouldDeferHydration ||= isReactiveProperty && !isReflectedReactiveProperty;
+
+ if (isReactiveProperty) {
instance.setProperty(name, value);
} else {
instance.setAttribute(name, value);
@@ -49,7 +57,7 @@ function* render(Component, attrs, slots) {
instance.connectedCallback();
- yield `<${tagName}`;
+ yield `<${tagName}${shouldDeferHydration ? ' defer-hydration' : ''}`;
yield* instance.renderAttributes();
yield `>`;
const shadowContents = instance.renderShadow({});
diff --git a/packages/integrations/lit/src/client.ts b/packages/integrations/lit/src/client.ts
new file mode 100644
index 000000000000..00f126e34d27
--- /dev/null
+++ b/packages/integrations/lit/src/client.ts
@@ -0,0 +1,25 @@
+export default (element: HTMLElement) =>
+ async (
+ Component: any,
+ props: Record,
+ ) => {
+ // Get the LitElement element instance (may or may not be upgraded).
+ const component = element.children[0] as HTMLElement;
+
+ // If there is no deferral of hydration, then all reactive properties are
+ // already serialzied as reflected attributes, or no reactive props were set
+ if (!component || !component.hasAttribute('defer-hydration')) {
+ return;
+ }
+
+ // Set properties on the LitElement instance for resuming hydration.
+ for (let [name, value] of Object.entries(props)) {
+ // Check if reactive property or class property.
+ if (name in Component.prototype) {
+ (component as any)[name] = value;
+ }
+ }
+
+ // Tell LitElement to resume hydration.
+ component.removeAttribute('defer-hydration');
+ };
diff --git a/packages/integrations/lit/src/index.ts b/packages/integrations/lit/src/index.ts
index 5b428ef8d9f4..de6d5b0f91c3 100644
--- a/packages/integrations/lit/src/index.ts
+++ b/packages/integrations/lit/src/index.ts
@@ -5,6 +5,7 @@ function getViteConfiguration() {
return {
optimizeDeps: {
include: [
+ '@astrojs/lit/dist/client.js',
'@astrojs/lit/client-shim.js',
'@astrojs/lit/hydration-support.js',
'@webcomponents/template-shadowroot/template-shadowroot.js',
@@ -34,6 +35,7 @@ export default function (): AstroIntegration {
addRenderer({
name: '@astrojs/lit',
serverEntrypoint: '@astrojs/lit/server.js',
+ clientEntrypoint: '@astrojs/lit/dist/client.js',
});
// Update the vite configuration.
updateConfig({