From f97f479bf626ea5cb38bf98f8e504afcd70e3944 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Thu, 10 Mar 2022 16:59:01 +0100 Subject: [PATCH] feat: improve React 18 support --- ...-80492b15-83a7-4ca1-8c1b-b5aeb0c3aa58.json | 7 +++++ ...-13bf5fac-a88b-41ad-98fc-a8df3be96e6d.json | 7 +++++ packages/core/src/__resetStyles.ts | 23 +++++++------- packages/core/src/__styles.ts | 14 ++++----- packages/core/src/index.ts | 1 + packages/core/src/insertionFactory.ts | 12 ++++++++ packages/core/src/makeResetStyles.ts | 17 ++++------- packages/core/src/makeStaticStyles.ts | 30 +++++++++---------- packages/core/src/makeStyles.ts | 17 ++++++----- packages/core/src/types.ts | 6 ++++ packages/react/src/RendererContext.tsx | 11 ++----- packages/react/src/__resetStyles.ts | 3 +- packages/react/src/__styles.ts | 3 +- packages/react/src/createDOMRenderer.test.tsx | 14 +++++++++ packages/react/src/insertionFactory.ts | 26 ++++++++++++++++ packages/react/src/makeResetStyles.ts | 5 ++-- packages/react/src/makeStaticStyles.ts | 5 ++-- packages/react/src/makeStyles.ts | 5 ++-- packages/react/src/useInsertionEffect.ts | 6 ++++ packages/react/src/utils/canUseDOM.ts | 6 ++++ 20 files changed, 146 insertions(+), 72 deletions(-) create mode 100644 change/@griffel-core-80492b15-83a7-4ca1-8c1b-b5aeb0c3aa58.json create mode 100644 change/@griffel-react-13bf5fac-a88b-41ad-98fc-a8df3be96e6d.json create mode 100644 packages/core/src/insertionFactory.ts create mode 100644 packages/react/src/insertionFactory.ts create mode 100644 packages/react/src/useInsertionEffect.ts create mode 100644 packages/react/src/utils/canUseDOM.ts diff --git a/change/@griffel-core-80492b15-83a7-4ca1-8c1b-b5aeb0c3aa58.json b/change/@griffel-core-80492b15-83a7-4ca1-8c1b-b5aeb0c3aa58.json new file mode 100644 index 000000000..93ccae5b2 --- /dev/null +++ b/change/@griffel-core-80492b15-83a7-4ca1-8c1b-b5aeb0c3aa58.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add API for styles insertion", + "packageName": "@griffel/core", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@griffel-react-13bf5fac-a88b-41ad-98fc-a8df3be96e6d.json b/change/@griffel-react-13bf5fac-a88b-41ad-98fc-a8df3be96e6d.json new file mode 100644 index 000000000..24e95bdec --- /dev/null +++ b/change/@griffel-react-13bf5fac-a88b-41ad-98fc-a8df3be96e6d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: improve React 18 support", + "packageName": "@griffel/react", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/core/src/__resetStyles.ts b/packages/core/src/__resetStyles.ts index c5b823dff..e3c5aad4a 100644 --- a/packages/core/src/__resetStyles.ts +++ b/packages/core/src/__resetStyles.ts @@ -1,25 +1,24 @@ import { DEBUG_RESET_CLASSES } from './constants'; +import { insertionFactory } from './insertionFactory'; import type { MakeResetStylesOptions } from './makeResetStyles'; +import type { GriffelInsertionFactory } from './types'; /** * @internal */ -export function __resetStyles(ltrClassName: string, rtlClassName: string | null, cssRules: string[]) { - const insertionCache: Record = {}; +export function __resetStyles( + ltrClassName: string, + rtlClassName: string | null, + cssRules: string[], + factory: GriffelInsertionFactory = insertionFactory, +) { + const insertStyles = factory(); function computeClassName(options: MakeResetStylesOptions): string { const { dir, renderer } = options; + const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName; - const isLTR = dir === 'ltr'; - // As RTL classes are different they should have a different cache key for insertion - const rendererId = isLTR ? renderer.id : renderer.id + 'r'; - - if (insertionCache[rendererId] === undefined) { - renderer.insertCSSRules({ r: cssRules! }); - insertionCache[rendererId] = true; - } - - const className = isLTR ? ltrClassName : rtlClassName || ltrClassName; + insertStyles(renderer, dir, { r: cssRules }); if (process.env.NODE_ENV !== 'production') { DEBUG_RESET_CLASSES[className] = 1; diff --git a/packages/core/src/__styles.ts b/packages/core/src/__styles.ts index 0d49cc76b..6fec3b824 100644 --- a/packages/core/src/__styles.ts +++ b/packages/core/src/__styles.ts @@ -1,6 +1,7 @@ import { debugData, isDevToolsEnabled, getSourceURLfromError } from './devtools'; +import { insertionFactory } from './insertionFactory'; import { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots'; -import type { CSSClassesMapBySlot, CSSRulesByBucket } from './types'; +import type { CSSClassesMapBySlot, CSSRulesByBucket, GriffelInsertionFactory } from './types'; import type { MakeStylesOptions } from './makeStyles'; /** @@ -11,8 +12,9 @@ import type { MakeStylesOptions } from './makeStyles'; export function __styles( classesMapBySlot: CSSClassesMapBySlot, cssRules: CSSRulesByBucket, + factory: GriffelInsertionFactory = insertionFactory, ) { - const insertionCache: Record = {}; + const insertStyles = factory(); let ltrClassNamesForSlots: Record | null = null; let rtlClassNamesForSlots: Record | null = null; @@ -24,10 +26,7 @@ export function __styles( function computeClasses(options: Pick): Record { const { dir, renderer } = options; - const isLTR = dir === 'ltr'; - // As RTL classes are different they should have a different cache key for insertion - const rendererId = isLTR ? renderer.id : renderer.id + 'r'; if (isLTR) { if (ltrClassNamesForSlots === null) { @@ -39,10 +38,7 @@ export function __styles( } } - if (insertionCache[rendererId] === undefined) { - renderer.insertCSSRules(cssRules!); - insertionCache[rendererId] = true; - } + insertStyles(renderer, dir, cssRules); const classNamesForSlots = isLTR ? (ltrClassNamesForSlots as Record) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0e2f2932d..1db8aa218 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -102,6 +102,7 @@ export { StyleBucketName, // Util GriffelRenderer, + GriffelInsertionFactory, } from './types'; // Private exports, are used by devtools diff --git a/packages/core/src/insertionFactory.ts b/packages/core/src/insertionFactory.ts new file mode 100644 index 000000000..1d5e3d61d --- /dev/null +++ b/packages/core/src/insertionFactory.ts @@ -0,0 +1,12 @@ +import { CSSRulesByBucket, GriffelInsertionFactory, GriffelRenderer } from './types'; + +export const insertionFactory: GriffelInsertionFactory = () => { + const insertionCache: Record = {}; + + return function insertStyles(renderer: GriffelRenderer, dir: string, cssRules: CSSRulesByBucket) { + if (insertionCache[renderer.id] === undefined) { + renderer.insertCSSRules(cssRules!); + insertionCache[renderer.id] = true; + } + }; +}; diff --git a/packages/core/src/makeResetStyles.ts b/packages/core/src/makeResetStyles.ts index b05604067..fc03b9372 100644 --- a/packages/core/src/makeResetStyles.ts +++ b/packages/core/src/makeResetStyles.ts @@ -1,16 +1,18 @@ import type { GriffelResetStyle } from '@griffel/style-types'; import { DEBUG_RESET_CLASSES } from './constants'; +import { insertionFactory } from './insertionFactory'; import { resolveResetStyleRules } from './runtime/resolveResetStyleRules'; import type { GriffelRenderer } from './types'; +import type { GriffelInsertionFactory } from './types'; export interface MakeResetStylesOptions { dir: 'ltr' | 'rtl'; renderer: GriffelRenderer; } -export function makeResetStyles(styles: GriffelResetStyle) { - const insertionCache: Record = {}; +export function makeResetStyles(styles: GriffelResetStyle, factory: GriffelInsertionFactory = insertionFactory) { + const insertStyles = factory(); let ltrClassName: string | null = null; let rtlClassName: string | null = null; @@ -24,16 +26,9 @@ export function makeResetStyles(styles: GriffelResetStyle) { [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles); } - const isLTR = dir === 'ltr'; - // As RTL classes are different they should have a different cache key for insertion - const rendererId = isLTR ? renderer.id : renderer.id + 'r'; + insertStyles(renderer, dir, { r: cssRules! }); - if (insertionCache[rendererId] === undefined) { - renderer.insertCSSRules({ r: cssRules! }); - insertionCache[rendererId] = true; - } - - const className = isLTR ? ltrClassName : rtlClassName || ltrClassName; + const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName; if (process.env.NODE_ENV !== 'production') { DEBUG_RESET_CLASSES[className] = 1; diff --git a/packages/core/src/makeStaticStyles.ts b/packages/core/src/makeStaticStyles.ts index b1ef3ed74..4bd51c1f0 100644 --- a/packages/core/src/makeStaticStyles.ts +++ b/packages/core/src/makeStaticStyles.ts @@ -1,31 +1,29 @@ import type { GriffelStaticStyles } from '@griffel/style-types'; +import { insertionFactory } from './insertionFactory'; import { resolveStaticStyleRules } from './runtime/resolveStaticStyleRules'; import type { GriffelRenderer } from './types'; +import type { GriffelInsertionFactory } from './types'; export interface MakeStaticStylesOptions { renderer: GriffelRenderer; } -/** - * Register static css. - * @param styles - styles object or string. - */ -export function makeStaticStyles(styles: GriffelStaticStyles | GriffelStaticStyles[]) { - const styleCache: Record = {}; +export function makeStaticStyles( + styles: GriffelStaticStyles | GriffelStaticStyles[], + factory: GriffelInsertionFactory = insertionFactory, +) { + const insertStyles = factory(); const stylesSet: GriffelStaticStyles[] = Array.isArray(styles) ? styles : [styles]; function useStaticStyles(options: MakeStaticStylesOptions): void { - const { renderer } = options; - const cacheKey = renderer.id; - - if (!styleCache[cacheKey]) { - renderer.insertCSSRules({ - // 👇 static rules should be inserted into default bucket - d: resolveStaticStyleRules(stylesSet), - }); - styleCache[cacheKey] = true; - } + insertStyles( + options.renderer, + // Static styles cannot be direction aware + 'ltr', + // 👇 static rules should be inserted into default bucket + { d: resolveStaticStyleRules(stylesSet) }, + ); } return useStaticStyles; diff --git a/packages/core/src/makeStyles.ts b/packages/core/src/makeStyles.ts index 136c80052..aa818ab74 100644 --- a/packages/core/src/makeStyles.ts +++ b/packages/core/src/makeStyles.ts @@ -1,15 +1,20 @@ import { debugData, isDevToolsEnabled, getSourceURLfromError } from './devtools'; +import { insertionFactory } from './insertionFactory'; import { resolveStyleRulesForSlots } from './resolveStyleRulesForSlots'; import { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots'; import type { CSSClassesMapBySlot, CSSRulesByBucket, GriffelRenderer, StylesBySlots } from './types'; +import type { GriffelInsertionFactory } from './types'; export interface MakeStylesOptions { dir: 'ltr' | 'rtl'; renderer: GriffelRenderer; } -export function makeStyles(stylesBySlots: StylesBySlots) { - const insertionCache: Record = {}; +export function makeStyles( + stylesBySlots: StylesBySlots, + factory: GriffelInsertionFactory = insertionFactory, +) { + const insertStyles = factory(); let classesMapBySlot: CSSClassesMapBySlot | null = null; let cssRules: CSSRulesByBucket | null = null; @@ -30,8 +35,6 @@ export function makeStyles(stylesBySlots: StylesB } const isLTR = dir === 'ltr'; - // As RTL classes are different they should have a different cache key for insertion - const rendererId = isLTR ? renderer.id : renderer.id + 'r'; if (isLTR) { if (ltrClassNamesForSlots === null) { @@ -43,10 +46,8 @@ export function makeStyles(stylesBySlots: StylesB } } - if (insertionCache[rendererId] === undefined) { - renderer.insertCSSRules(cssRules!); - insertionCache[rendererId] = true; - } + insertStyles(renderer, dir, cssRules!); + const classNamesForSlots = isLTR ? (ltrClassNamesForSlots as Record) : (rtlClassNamesForSlots as Record); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 408378b9f..11b99fabd 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -87,6 +87,12 @@ export type CSSRulesByBucket = { c?: CSSBucketEntry[]; }; +export type GriffelInsertionFactory = () => ( + renderer: GriffelRenderer, + dir: 'rtl' | 'ltr', + cssRules: CSSRulesByBucket, +) => void; + /** @internal */ export type CSSBucketEntry = string | [string, Record]; diff --git a/packages/react/src/RendererContext.tsx b/packages/react/src/RendererContext.tsx index 62e470e8a..dcf53d563 100644 --- a/packages/react/src/RendererContext.tsx +++ b/packages/react/src/RendererContext.tsx @@ -1,6 +1,8 @@ import { createDOMRenderer, rehydrateRendererCache } from '@griffel/core'; -import * as React from 'react'; import type { GriffelRenderer } from '@griffel/core'; +import * as React from 'react'; + +import { canUseDOM } from './utils/canUseDOM'; export interface RendererProviderProps { /** An instance of Griffel renderer. */ @@ -17,13 +19,6 @@ export interface RendererProviderProps { children: React.ReactNode; } -/** - * Verifies if an application can use DOM. - */ -function canUseDOM(): boolean { - return typeof window !== 'undefined' && !!(window.document && window.document.createElement); -} - /** * @private */ diff --git a/packages/react/src/__resetStyles.ts b/packages/react/src/__resetStyles.ts index 7bad4e03b..42de81760 100644 --- a/packages/react/src/__resetStyles.ts +++ b/packages/react/src/__resetStyles.ts @@ -1,5 +1,6 @@ import { __resetStyles as vanillaResetStyles } from '@griffel/core'; +import { insertionFactory } from './insertionFactory'; import { useRenderer } from './RendererContext'; import { useTextDirection } from './TextDirectionContext'; @@ -10,7 +11,7 @@ import { useTextDirection } from './TextDirectionContext'; */ // eslint-disable-next-line @typescript-eslint/naming-convention export function __resetStyles(ltrClassName: string, rtlClassName: string | null, cssRules: string[]) { - const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules); + const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules, insertionFactory); return function useClasses(): string { const dir = useTextDirection(); diff --git a/packages/react/src/__styles.ts b/packages/react/src/__styles.ts index 345627ec6..bcfb237b3 100644 --- a/packages/react/src/__styles.ts +++ b/packages/react/src/__styles.ts @@ -1,6 +1,7 @@ import { __styles as vanillaStyles } from '@griffel/core'; import type { CSSClassesMapBySlot, CSSRulesByBucket } from '@griffel/core'; +import { insertionFactory } from './insertionFactory'; import { useRenderer } from './RendererContext'; import { useTextDirection } from './TextDirectionContext'; @@ -14,7 +15,7 @@ export function __styles( classesMapBySlot: CSSClassesMapBySlot, cssRules: CSSRulesByBucket, ) { - const getStyles = vanillaStyles(classesMapBySlot, cssRules); + const getStyles = vanillaStyles(classesMapBySlot, cssRules, insertionFactory); return function useClasses(): Record { const dir = useTextDirection(); diff --git a/packages/react/src/createDOMRenderer.test.tsx b/packages/react/src/createDOMRenderer.test.tsx index 89b6fa0a3..611b4e01d 100644 --- a/packages/react/src/createDOMRenderer.test.tsx +++ b/packages/react/src/createDOMRenderer.test.tsx @@ -8,6 +8,13 @@ import { makeStyles } from './makeStyles'; import { makeResetStyles } from './makeResetStyles'; import { RendererProvider } from './RendererContext'; import { renderToStyleElements } from './renderToStyleElements'; +import { useInsertionEffect as _useInsertionEffect } from './useInsertionEffect'; + +jest.mock('./useInsertionEffect', () => ({ + useInsertionEffect: jest.fn(), +})); + +const useInsertionEffect = _useInsertionEffect as jest.MockedFunction; describe('createDOMRenderer', () => { it('rehydrateCache() avoids double insertion', () => { @@ -46,6 +53,11 @@ describe('createDOMRenderer', () => { // A "server" renders components to static HTML that will be transferred to a client // + // Heads up! + // Mock there is need as this test is executed in DOM environment and uses "useInsertionEffect". + // However, "useInsertionEffect" will not be called in "renderToStaticMarkup()". + useInsertionEffect.mockImplementation(fn => fn()); + const componentHTML = renderToStaticMarkup( @@ -53,6 +65,8 @@ describe('createDOMRenderer', () => { ); const stylesHTML = renderToStaticMarkup(<>{renderToStyleElements(serverRenderer)}); + useInsertionEffect.mockImplementation(React.useInsertionEffect); + // Ensure that all styles are inserted into the cache expect(serverRenderer.insertionCache).toMatchInlineSnapshot(` Object { diff --git a/packages/react/src/insertionFactory.ts b/packages/react/src/insertionFactory.ts new file mode 100644 index 000000000..df38c9d15 --- /dev/null +++ b/packages/react/src/insertionFactory.ts @@ -0,0 +1,26 @@ +import type { CSSRulesByBucket, GriffelInsertionFactory, GriffelRenderer } from '@griffel/core'; + +import { canUseDOM } from './utils/canUseDOM'; +import { useInsertionEffect } from './useInsertionEffect'; + +export const insertionFactory: GriffelInsertionFactory = () => { + const insertionCache: Record = {}; + + return function insert(renderer: GriffelRenderer, dir: string, cssRules: CSSRulesByBucket) { + if (useInsertionEffect) { + // Even if `useInsertionEffect` is available, we can't use it in SSR as it will not be executed + if (canUseDOM()) { + useInsertionEffect(() => { + renderer.insertCSSRules(cssRules!); + }, [renderer, cssRules]); + + return; + } + } + + if (insertionCache[renderer.id] === undefined) { + renderer.insertCSSRules(cssRules!); + insertionCache[renderer.id] = true; + } + }; +}; diff --git a/packages/react/src/makeResetStyles.ts b/packages/react/src/makeResetStyles.ts index a42b98014..7ebf072aa 100644 --- a/packages/react/src/makeResetStyles.ts +++ b/packages/react/src/makeResetStyles.ts @@ -1,12 +1,13 @@ import { makeResetStyles as vanillaMakeResetStyles } from '@griffel/core'; import type { GriffelResetStyle } from '@griffel/core'; -import { isInsideComponent } from './utils/isInsideComponent'; +import { insertionFactory } from './insertionFactory'; import { useRenderer } from './RendererContext'; import { useTextDirection } from './TextDirectionContext'; +import { isInsideComponent } from './utils/isInsideComponent'; export function makeResetStyles(styles: GriffelResetStyle) { - const getStyles = vanillaMakeResetStyles(styles); + const getStyles = vanillaMakeResetStyles(styles, insertionFactory); if (process.env.NODE_ENV !== 'production') { if (isInsideComponent()) { diff --git a/packages/react/src/makeStaticStyles.ts b/packages/react/src/makeStaticStyles.ts index dd3bc29a3..e60611b35 100644 --- a/packages/react/src/makeStaticStyles.ts +++ b/packages/react/src/makeStaticStyles.ts @@ -1,10 +1,11 @@ import { makeStaticStyles as vanillaMakeStaticStyles } from '@griffel/core'; +import type { GriffelStaticStyles, MakeStaticStylesOptions } from '@griffel/core'; +import { insertionFactory } from './insertionFactory'; import { useRenderer } from './RendererContext'; -import type { GriffelStaticStyles, MakeStaticStylesOptions } from '@griffel/core'; export function makeStaticStyles(styles: GriffelStaticStyles | GriffelStaticStyles[]) { - const getStyles = vanillaMakeStaticStyles(styles); + const getStyles = vanillaMakeStaticStyles(styles, insertionFactory); if (process.env.NODE_ENV === 'test') { // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/packages/react/src/makeStyles.ts b/packages/react/src/makeStyles.ts index f999675ad..e4090d5c1 100644 --- a/packages/react/src/makeStyles.ts +++ b/packages/react/src/makeStyles.ts @@ -1,12 +1,13 @@ import { makeStyles as vanillaMakeStyles } from '@griffel/core'; import type { GriffelStyle } from '@griffel/core'; -import { isInsideComponent } from './utils/isInsideComponent'; +import { insertionFactory } from './insertionFactory'; import { useRenderer } from './RendererContext'; import { useTextDirection } from './TextDirectionContext'; +import { isInsideComponent } from './utils/isInsideComponent'; export function makeStyles(stylesBySlots: Record) { - const getStyles = vanillaMakeStyles(stylesBySlots); + const getStyles = vanillaMakeStyles(stylesBySlots, insertionFactory); if (process.env.NODE_ENV !== 'production') { if (isInsideComponent()) { diff --git a/packages/react/src/useInsertionEffect.ts b/packages/react/src/useInsertionEffect.ts new file mode 100644 index 000000000..7837a993d --- /dev/null +++ b/packages/react/src/useInsertionEffect.ts @@ -0,0 +1,6 @@ +import * as React from 'react'; + +// Hack to make sure that `useInsertionEffect` will not cause bundling issues in older React versions +export const useInsertionEffect: typeof React.useInsertionEffect | undefined = + // @ts-expect-error + React['useInsertion' + 'Effect'] ? React['useInsertion' + 'Effect'] : undefined; diff --git a/packages/react/src/utils/canUseDOM.ts b/packages/react/src/utils/canUseDOM.ts new file mode 100644 index 000000000..d11d3ff72 --- /dev/null +++ b/packages/react/src/utils/canUseDOM.ts @@ -0,0 +1,6 @@ +/** + * Verifies if an application can use DOM. + */ +export function canUseDOM(): boolean { + return typeof window !== 'undefined' && !!(window.document && window.document.createElement); +}