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/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..d1ef82b7b --- /dev/null +++ b/packages/react/src/insertionFactory.ts @@ -0,0 +1,27 @@ +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()) { + // eslint-disable-next-line react-hooks/rules-of-hooks + 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..fe5e8c5dc --- /dev/null +++ b/packages/react/src/useInsertionEffect.ts @@ -0,0 +1,6 @@ +import * as React from 'react'; + +export const useInsertionEffect: typeof React.useInsertionEffect | undefined = + // @ts-expect-error Hack to make sure that `useInsertionEffect` will not cause bundling issues in older React versions + // eslint-disable-next-line no-useless-concat + 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); +}