diff --git a/packages/qwik/src/core/component/component-ctx.ts b/packages/qwik/src/core/component/component-ctx.ts index 4aa0abf16a5..dfb037c6c26 100644 --- a/packages/qwik/src/core/component/component-ctx.ts +++ b/packages/qwik/src/core/component/component-ctx.ts @@ -1,10 +1,10 @@ import { assertDefined } from '../assert/assert'; -import { appendStyle, RenderContext } from '../render/cursor'; +import type { RenderContext } from '../render/cursor'; import { visitJsxNode } from '../render/render'; import { ComponentScopedStyles, QHostAttr, RenderEvent } from '../util/markers'; import { promiseAll, then } from '../util/promises'; import { styleContent, styleHost } from './qrl-styles'; -import { isStyleTask, newInvokeContext } from '../use/use-core'; +import { newInvokeContext } from '../use/use-core'; import { processNode } from '../render/jsx/jsx-runtime'; import { logDebug, logError } from '../util/log'; import type { ValueOrPromise } from '../util/types'; @@ -55,12 +55,7 @@ export const renderComponent = (rctx: RenderContext, ctx: QContext): ValueOrProm (jsxNode) => { rctx.hostElements.add(hostElement); const waitOnPromise = promiseAll(waitOn); - return then(waitOnPromise, (waitOnResolved) => { - waitOnResolved.forEach((task) => { - if (isStyleTask(task)) { - appendStyle(rctx, hostElement, task); - } - }); + return then(waitOnPromise, () => { if (typeof jsxNode === 'function') { ctx.dirty = false; jsxNode = jsxNode(); diff --git a/packages/qwik/src/core/component/component.public.ts b/packages/qwik/src/core/component/component.public.ts index 3edd96b0f16..88924a8758f 100644 --- a/packages/qwik/src/core/component/component.public.ts +++ b/packages/qwik/src/core/component/component.public.ts @@ -2,7 +2,7 @@ import { toQrlOrError } from '../import/qrl'; import { $, implicit$FirstArg, QRL } from '../import/qrl.public'; import { qPropWriteQRL } from '../props/props-on'; import type { JSXNode } from '../render/jsx/types/jsx-node'; -import { StyleAppend, useWaitOn } from '../use/use-core'; +import { useRenderContext, useWaitOn } from '../use/use-core'; import { useHostElement } from '../use/use-host-element.public'; import { ComponentScopedStyles, OnRenderProp } from '../util/markers'; import { styleKey } from './qrl-styles'; @@ -14,6 +14,7 @@ import { jsx } from '../render/jsx/jsx-runtime'; import { useSequentialScope } from '../use/use-store.public'; import { WatchDescriptor, WatchFlags } from '../watch/watch.public'; import type { MutableWrapper } from '../object/q-object'; +import { appendStyle, hasStyle } from '../render/cursor'; // // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! @@ -578,26 +579,30 @@ export interface RenderFactoryOutput { } function _useStyles(styles: QRL, scoped: boolean) { - const [style, setStyle] = useSequentialScope(); + const [style, setStyle, index] = useSequentialScope(); if (style === true) { return; } setStyle(true); + const renderCtx = useRenderContext(); const styleQrl = toQrlOrError(styles); - const styleId = styleKey(styleQrl); + const styleId = styleKey(styleQrl, index); const hostElement = useHostElement(); if (scoped) { hostElement.setAttribute(ComponentScopedStyles, styleId); } - useWaitOn( - styleQrl.resolve(hostElement).then((styleText) => { - const task: StyleAppend = { - type: 'style', - styleId, - content: scoped ? styleText.replace(/�/g, styleId) : styleText, - }; - return task; - }) - ); + if (!hasStyle(renderCtx, styleId)) { + useWaitOn( + styleQrl.resolve(hostElement).then((styleText) => { + if (!hasStyle(renderCtx, styleId)) { + appendStyle(renderCtx, hostElement, { + type: 'style', + styleId, + content: scoped ? styleText.replace(/�/g, styleId) : styleText, + }); + } + }) + ); + } } diff --git a/packages/qwik/src/core/component/qrl-styles.ts b/packages/qwik/src/core/component/qrl-styles.ts index e632d5a9f70..a4e7e3bd38b 100644 --- a/packages/qwik/src/core/component/qrl-styles.ts +++ b/packages/qwik/src/core/component/qrl-styles.ts @@ -5,26 +5,20 @@ import { hashCode } from '../util/hash_code'; /** * @public */ -export function styleKey(qStyles: QRLInternal): string; -export function styleKey(qStyles: QRLInternal | null): string | null; -export function styleKey(qStyles: QRLInternal | null): string | null { - return qStyles && String(hashCode(qStyles.getCanonicalSymbol())); +export function styleKey(qStyles: QRLInternal, index: number): string { + return `${hashCode(qStyles.getCanonicalSymbol())}-${index}`; } /** * @public */ -export function styleHost(styleId: string): string; -export function styleHost(styleId: string | undefined): string | undefined; -export function styleHost(styleId: string | undefined): string | undefined { - return styleId && ComponentStylesPrefixHost + styleId; +export function styleHost(styleId: string): string { + return ComponentStylesPrefixHost + styleId; } /** * @public */ -export function styleContent(styleId: string): string; -export function styleContent(styleId: string | undefined): string | undefined; -export function styleContent(styleId: string | undefined): string | undefined { - return styleId && ComponentStylesPrefixContent + styleId; +export function styleContent(styleId: string): string { + return ComponentStylesPrefixContent + styleId; } diff --git a/packages/qwik/src/core/render/cursor.ts b/packages/qwik/src/core/render/cursor.ts index adc3e6994a5..5c8a6b1c7ac 100644 --- a/packages/qwik/src/core/render/cursor.ts +++ b/packages/qwik/src/core/render/cursor.ts @@ -25,9 +25,10 @@ import { qDev } from '../util/qdev'; import { qError, QError } from '../error/error'; import { fromCamelToKebabCase } from '../util/case'; import type { OnRenderFn } from '../component/component.public'; -import { CONTAINER, StyleAppend } from '../use/use-core'; +import { CONTAINER, isStyleTask, StyleAppend } from '../use/use-core'; import type { Ref } from '../use/use-store.public'; import type { SubscriptionManager } from '../object/q-object'; +import { getDocument } from '../util/dom'; export const SVG_NS = 'http://www.w3.org/2000/svg'; @@ -796,12 +797,10 @@ export function appendStyle(ctx: RenderContext, hostElement: Element, styleTask: const containerEl = ctx.containerEl; const stylesParent = ctx.doc.documentElement === containerEl ? ctx.doc.head ?? containerEl : containerEl; - if (!stylesParent.querySelector(`style[q\\:style="${styleTask.styleId}"]`)) { - const style = ctx.doc.createElement('style'); - style.setAttribute('q:style', styleTask.styleId); - style.textContent = styleTask.content; - stylesParent.insertBefore(style, stylesParent.firstChild); - } + const style = ctx.doc.createElement('style'); + style.setAttribute('q:style', styleTask.styleId); + style.textContent = styleTask.content; + stylesParent.insertBefore(style, stylesParent.firstChild); }; ctx.operations.push({ el: hostElement, @@ -810,6 +809,26 @@ export function appendStyle(ctx: RenderContext, hostElement: Element, styleTask: fn, }); } + +export function hasStyle(ctx: RenderContext, styleId: string) { + const containerEl = ctx.containerEl; + const doc = getDocument(containerEl); + const hasOperation = ctx.operations.some((op) => { + if (op.operation === 'append-style') { + const s = op.args[0]; + if (isStyleTask(s)) { + return s.styleId === styleId; + } + } + return false; + }); + if (hasOperation) { + return true; + } + const stylesParent = doc.documentElement === containerEl ? doc.head ?? containerEl : containerEl; + return !!stylesParent.querySelector(`style[q\\:style="${styleId}"]`); +} + function prepend(ctx: RenderContext, parent: Element, newChild: Node) { const fn = () => { parent.insertBefore(newChild, parent.firstChild); diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index c111c288db6..fe3809edaa9 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -7,7 +7,6 @@ import { getDocument } from '../util/dom'; import type { QRL } from '../import/qrl.public'; import type { Subscriber } from './use-subscriber'; import type { RenderContext } from '../render/cursor'; -import type { ContainerState } from '../render/notify-render'; declare const document: QwikDocument; @@ -149,11 +148,11 @@ export function getContainer(el: Element): Element | null { return container; } -export function useContainerState(): ContainerState { +export function useRenderContext(): RenderContext { const ctx = getInvokeContext(); - const containerState = ctx.renderCtx?.containerState; - if (!containerState) { - throw new Error('Cant access containerState for existing context'); + const renderCtx = ctx.renderCtx; + if (!renderCtx) { + throw new Error('Cant access renderCtx for existing context'); } - return containerState; + return renderCtx; } diff --git a/packages/qwik/src/core/use/use-store.public.ts b/packages/qwik/src/core/use/use-store.public.ts index 1d5133e8482..10727b77e30 100644 --- a/packages/qwik/src/core/use/use-store.public.ts +++ b/packages/qwik/src/core/use/use-store.public.ts @@ -1,5 +1,5 @@ import { qObject } from '../object/q-object'; -import { getInvokeContext, useContainerState } from './use-core'; +import { getInvokeContext, useRenderContext } from './use-core'; import { useHostElement } from './use-host-element.public'; import { getContext } from '../props/props'; import { wrapSubscriber } from './use-subscriber'; @@ -76,7 +76,7 @@ export function useStore(initialState: STATE | (() => STAT return wrapSubscriber(store, hostElement); } - const containerState = useContainerState(); + const containerState = useRenderContext().containerState; const value = typeof initialState === 'function' ? (initialState as Function)() : initialState; const newStore = qObject(value, containerState); setStore(newStore); diff --git a/packages/qwik/src/core/watch/watch.public.ts b/packages/qwik/src/core/watch/watch.public.ts index 4afd68562cc..c21a3172e33 100644 --- a/packages/qwik/src/core/watch/watch.public.ts +++ b/packages/qwik/src/core/watch/watch.public.ts @@ -1,7 +1,7 @@ import { noSerialize, NoSerialize, notifyWatch } from '../object/q-object'; import { implicit$FirstArg, QRL } from '../import/qrl.public'; import { getContext } from '../props/props'; -import { newInvokeContext, useContainerState, useWaitOn } from '../use/use-core'; +import { newInvokeContext, useRenderContext, useWaitOn } from '../use/use-core'; import { useHostElement } from '../use/use-host-element.public'; import { logDebug, logError } from '../util/log'; import { then } from '../util/promises'; @@ -139,7 +139,7 @@ export function useWatchQrl(qrl: QRL, opts?: UseEffectOptions): void { const [watch, setWatch, i] = useSequentialScope(); if (!watch) { const el = useHostElement(); - const containerState = useContainerState(); + const containerState = useRenderContext().containerState; const watch: WatchDescriptor = { qrl, el, diff --git a/starters/apps/e2e/src/components/styles/child.css b/starters/apps/e2e/src/components/styles/child.css new file mode 100644 index 00000000000..f34bf715c6c --- /dev/null +++ b/starters/apps/e2e/src/components/styles/child.css @@ -0,0 +1,3 @@ +.child { + font-size: 20px; +} diff --git a/starters/apps/e2e/src/components/styles/child2.css b/starters/apps/e2e/src/components/styles/child2.css new file mode 100644 index 00000000000..05a836ba563 --- /dev/null +++ b/starters/apps/e2e/src/components/styles/child2.css @@ -0,0 +1,3 @@ +.child2 { + font-size: 10px; +} diff --git a/starters/apps/e2e/src/components/styles/parent.css b/starters/apps/e2e/src/components/styles/parent.css new file mode 100644 index 00000000000..c9344550d1d --- /dev/null +++ b/starters/apps/e2e/src/components/styles/parent.css @@ -0,0 +1,3 @@ +.parent { + font-size: 200px; +} diff --git a/starters/apps/e2e/src/components/styles/styles.tsx b/starters/apps/e2e/src/components/styles/styles.tsx new file mode 100644 index 00000000000..734478a7bf1 --- /dev/null +++ b/starters/apps/e2e/src/components/styles/styles.tsx @@ -0,0 +1,29 @@ +import { component$, Host, useStore, useStyles$ } from '@builder.io/qwik'; +import parent from './parent.css'; +import child from './child.css'; +import child2 from './child2.css'; + +export const Styles = component$(() => { + useStyles$(parent); + const store = useStore({ + count: 10, + }); + return ( + + Parent + + {Array.from({ length: store.count }).map(() => ( + + ))} + + ); +}); + +export const Child = component$(() => { + useStyles$(child); + useStyles$(child2); + + return Child; +}); diff --git a/starters/apps/e2e/src/entry.ssr.tsx b/starters/apps/e2e/src/entry.ssr.tsx index 75f51c5b245..14b16b4bfcf 100644 --- a/starters/apps/e2e/src/entry.ssr.tsx +++ b/starters/apps/e2e/src/entry.ssr.tsx @@ -13,6 +13,7 @@ import { Watch } from './components/watch/watch'; import { EffectClient } from './components/effect-client/effect-client'; import { ContextRoot } from './components/context/context'; import { Toggle } from './components/toggle/toggle'; +import { Styles } from './components/styles/styles'; /** * Entry point for server-side pre-rendering. @@ -36,6 +37,7 @@ export function render(opts: RenderToStringOptions) { '/e2e/effect-client': () => , '/e2e/context': () => , '/e2e/toggle': () => , + '/e2e/styles': () => , }; const Test = tests[url.pathname]; diff --git a/starters/apps/e2e/src/root.tsx b/starters/apps/e2e/src/root.tsx index 6025aaf6808..23a1ad1a18f 100644 --- a/starters/apps/e2e/src/root.tsx +++ b/starters/apps/e2e/src/root.tsx @@ -41,6 +41,9 @@ export const Root = component$(() => {

Toggle

+

+ Styles +

); });