From 9547ef9a2accfb27063c81b11cef7b7600fc3d61 Mon Sep 17 00:00:00 2001 From: Anton Evzhakov Date: Tue, 23 Jun 2026 13:08:02 +0300 Subject: [PATCH] fix(react): improve lazy styled specificity (#1415) --- .changeset/lazy-styled-specificity.md | 5 ++++ packages/react/src/processors/styled.ts | 14 +++++++++++ packages/testkit/src/babel.test.ts | 32 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 .changeset/lazy-styled-specificity.md diff --git a/.changeset/lazy-styled-specificity.md b/.changeset/lazy-styled-specificity.md new file mode 100644 index 000000000..48937000a --- /dev/null +++ b/.changeset/lazy-styled-specificity.md @@ -0,0 +1,5 @@ +--- +"@linaria/react": patch +--- + +Increase selector specificity when styling React.lazy targets so wrapper styles can override lazy-loaded component CSS. diff --git a/packages/react/src/processors/styled.ts b/packages/react/src/processors/styled.ts index 9c02f9d80..a2e989f4b 100644 --- a/packages/react/src/processors/styled.ts +++ b/packages/react/src/processors/styled.ts @@ -106,6 +106,16 @@ type RawStringLiteral = StringLiteral & { const staticClassSelector = (className: string): string => `.${className}`; +const isReactLazyValue = (value: unknown): boolean => { + if (typeof value !== 'object' || value === null) { + return false; + } + + return ( + (value as { $$typeof?: unknown }).$$typeof === Symbol.for('react.lazy') + ); +}; + const isStaticStyledValue = (value: unknown): value is StaticStyledValue => { if (typeof value !== 'object' || value === null) { return false; @@ -376,6 +386,10 @@ export default class StyledProcessor extends TaggedTemplateProcessor { value = value.__wyw_meta.extends; } + if (isReactLazyValue(value)) { + selector += `.${this.className}`; + } + rules[selector] = { cssText, className: this.className, diff --git a/packages/testkit/src/babel.test.ts b/packages/testkit/src/babel.test.ts index e8992c06c..af9764c08 100644 --- a/packages/testkit/src/babel.test.ts +++ b/packages/testkit/src/babel.test.ts @@ -1859,6 +1859,38 @@ describe('strategy shaker', () => { expect(metadata).toMatchSnapshot(); }); + it('adds specificity when wrapping a lazy component', async () => { + const { metadata } = await transform( + dedent` + import { styled } from '@linaria/react'; + + const lazy = (ctor) => ({ + $$typeof: Symbol.for('react.lazy'), + _ctor: ctor, + _status: -1, + _result: null, + }); + + const Title = styled.h1\` + color: red; + \`; + + const LazyTitle = lazy(() => Promise.resolve({ default: Title })); + + export const BlueTitle = styled(LazyTitle)\` + color: blue; + \`; + `, + [evaluator] + ); + + const selector = Object.keys(metadata.wywInJS.rules ?? {}).find((rule) => + rule.startsWith('.BlueTitle_') + ); + + expect(selector).toMatch(/^(\.BlueTitle_[^.]+)\1$/); + }); + it('handles indirect wrapping another styled component', async () => { const { code, metadata } = await transform( dedent`