Skip to content

Commit d12c92d

Browse files
committed
chore: improve React 18 support
1 parent 9f7710e commit d12c92d

15 files changed

+147
-16
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "chore: improve React 18 support by using useInsertionEffect",
4+
"packageName": "@griffel/react",
5+
"email": "olfedias@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react/src/RendererContext.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createDOMRenderer, rehydrateRendererCache } from '@griffel/core';
2-
import * as React from 'react';
32
import type { GriffelRenderer } from '@griffel/core';
3+
import * as React from 'react';
4+
5+
import { canUseDOM } from './utils/canUseDOM';
46

57
export interface RendererProviderProps {
68
/** An instance of Griffel renderer. */
@@ -17,13 +19,6 @@ export interface RendererProviderProps {
1719
children: React.ReactNode;
1820
}
1921

20-
/**
21-
* Verifies if an application can use DOM.
22-
*/
23-
function canUseDOM(): boolean {
24-
return typeof window !== 'undefined' && !!(window.document && window.document.createElement);
25-
}
26-
2722
/**
2823
* @private
2924
*/

packages/react/src/__resetStyles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { __resetStyles as vanillaResetStyles } from '@griffel/core';
22

3+
import { insertionFactory } from './insertionFactory';
34
import { useRenderer } from './RendererContext';
45
import { useTextDirection } from './TextDirectionContext';
56

@@ -10,7 +11,7 @@ import { useTextDirection } from './TextDirectionContext';
1011
*/
1112
// eslint-disable-next-line @typescript-eslint/naming-convention
1213
export function __resetStyles(ltrClassName: string, rtlClassName: string | null, cssRules: string[]) {
13-
const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules);
14+
const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules, insertionFactory);
1415

1516
return function useClasses(): string {
1617
const dir = useTextDirection();

packages/react/src/__styles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { __styles as vanillaStyles } from '@griffel/core';
22
import type { CSSClassesMapBySlot, CSSRulesByBucket } from '@griffel/core';
33

4+
import { insertionFactory } from './insertionFactory';
45
import { useRenderer } from './RendererContext';
56
import { useTextDirection } from './TextDirectionContext';
67

@@ -14,7 +15,7 @@ export function __styles<Slots extends string>(
1415
classesMapBySlot: CSSClassesMapBySlot<Slots>,
1516
cssRules: CSSRulesByBucket,
1617
) {
17-
const getStyles = vanillaStyles(classesMapBySlot, cssRules);
18+
const getStyles = vanillaStyles(classesMapBySlot, cssRules, insertionFactory);
1819

1920
return function useClasses(): Record<Slots, string> {
2021
const dir = useTextDirection();

packages/react/src/createDOMRenderer.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import { makeStyles } from './makeStyles';
88
import { makeResetStyles } from './makeResetStyles';
99
import { RendererProvider } from './RendererContext';
1010
import { renderToStyleElements } from './renderToStyleElements';
11+
import { useInsertionEffect as _useInsertionEffect } from './useInsertionEffect';
12+
13+
jest.mock('./useInsertionEffect', () => ({
14+
useInsertionEffect: jest.fn(),
15+
}));
16+
17+
const useInsertionEffect = _useInsertionEffect as jest.MockedFunction<typeof React.useInsertionEffect>;
1118

1219
describe('createDOMRenderer', () => {
1320
it('rehydrateCache() avoids double insertion', () => {
@@ -46,13 +53,20 @@ describe('createDOMRenderer', () => {
4653
// A "server" renders components to static HTML that will be transferred to a client
4754
//
4855

56+
// Heads up!
57+
// Mock there is need as this test is executed in DOM environment and uses "useInsertionEffect".
58+
// However, "useInsertionEffect" will not be called in "renderToStaticMarkup()".
59+
useInsertionEffect.mockImplementation(fn => fn());
60+
4961
const componentHTML = renderToStaticMarkup(
5062
<RendererProvider renderer={serverRenderer}>
5163
<ExampleComponent />
5264
</RendererProvider>,
5365
);
5466
const stylesHTML = renderToStaticMarkup(<>{renderToStyleElements(serverRenderer)}</>);
5567

68+
useInsertionEffect.mockImplementation(React.useInsertionEffect);
69+
5670
// Ensure that all styles are inserted into the cache
5771
expect(serverRenderer.insertionCache).toMatchInlineSnapshot(`
5872
Object {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* @jest-environment node
3+
*/
4+
5+
// 👆 this is intentionally to test in SSR like environment
6+
7+
import type { GriffelRenderer } from '@griffel/core';
8+
import * as React from 'react';
9+
10+
import { insertionFactory } from './insertionFactory';
11+
12+
describe('insertionFactory (node)', () => {
13+
it('does not use insertionEffect', () => {
14+
const useInsertionEffect = jest.spyOn(React, 'useInsertionEffect');
15+
16+
const renderer: Partial<GriffelRenderer> = { id: 'a', insertCSSRules: jest.fn() };
17+
const insertStyles = insertionFactory();
18+
19+
insertStyles(renderer as GriffelRenderer, { d: ['a'] });
20+
21+
expect(useInsertionEffect).not.toHaveBeenCalled();
22+
expect(renderer.insertCSSRules).toHaveBeenCalledTimes(1);
23+
});
24+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { GriffelRenderer } from '@griffel/core';
2+
3+
import { insertionFactory } from './insertionFactory';
4+
import { useInsertionEffect as _useInsertionEffect } from './useInsertionEffect';
5+
import * as React from 'react';
6+
7+
jest.mock('./useInsertionEffect', () => ({
8+
useInsertionEffect: jest.fn().mockImplementation(fn => fn()),
9+
}));
10+
11+
const useInsertionEffect = _useInsertionEffect as jest.MockedFunction<typeof React.useInsertionEffect>;
12+
13+
describe('canUseDOM', () => {
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it('uses "useInsertionEffect" when available', () => {
19+
const renderer: Partial<GriffelRenderer> = { insertCSSRules: jest.fn() };
20+
const insertStyles = insertionFactory();
21+
22+
insertStyles(renderer as GriffelRenderer, { d: ['a'] });
23+
24+
expect(useInsertionEffect).toHaveBeenCalledTimes(1);
25+
expect(renderer.insertCSSRules).toHaveBeenCalledTimes(1);
26+
});
27+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { CSSRulesByBucket, GriffelInsertionFactory, GriffelRenderer } from '@griffel/core';
2+
3+
import { canUseDOM } from './utils/canUseDOM';
4+
import { useInsertionEffect } from './useInsertionEffect';
5+
6+
export const insertionFactory: GriffelInsertionFactory = () => {
7+
const insertionCache: Record<string, boolean> = {};
8+
9+
return function insert(renderer: GriffelRenderer, cssRules: CSSRulesByBucket) {
10+
if (useInsertionEffect) {
11+
// Even if `useInsertionEffect` is available, we can't use it in SSR as it will not be executed
12+
if (canUseDOM()) {
13+
// eslint-disable-next-line react-hooks/rules-of-hooks
14+
useInsertionEffect(() => {
15+
renderer.insertCSSRules(cssRules!);
16+
}, [renderer, cssRules]);
17+
18+
return;
19+
}
20+
}
21+
22+
if (insertionCache[renderer.id] === undefined) {
23+
renderer.insertCSSRules(cssRules!);
24+
insertionCache[renderer.id] = true;
25+
}
26+
};
27+
};

packages/react/src/makeResetStyles.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { makeResetStyles as vanillaMakeResetStyles } from '@griffel/core';
22
import type { GriffelResetStyle } from '@griffel/core';
33

4-
import { isInsideComponent } from './utils/isInsideComponent';
4+
import { insertionFactory } from './insertionFactory';
55
import { useRenderer } from './RendererContext';
66
import { useTextDirection } from './TextDirectionContext';
7+
import { isInsideComponent } from './utils/isInsideComponent';
78

89
export function makeResetStyles(styles: GriffelResetStyle) {
9-
const getStyles = vanillaMakeResetStyles(styles);
10+
const getStyles = vanillaMakeResetStyles(styles, insertionFactory);
1011

1112
if (process.env.NODE_ENV !== 'production') {
1213
if (isInsideComponent()) {

packages/react/src/makeStaticStyles.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { makeStaticStyles as vanillaMakeStaticStyles } from '@griffel/core';
2+
import type { GriffelStaticStyles, MakeStaticStylesOptions } from '@griffel/core';
23

4+
import { insertionFactory } from './insertionFactory';
35
import { useRenderer } from './RendererContext';
4-
import type { GriffelStaticStyles, MakeStaticStylesOptions } from '@griffel/core';
56

67
export function makeStaticStyles(styles: GriffelStaticStyles | GriffelStaticStyles[]) {
7-
const getStyles = vanillaMakeStaticStyles(styles);
8+
const getStyles = vanillaMakeStaticStyles(styles, insertionFactory);
89

910
if (process.env.NODE_ENV === 'test') {
1011
// eslint-disable-next-line @typescript-eslint/no-empty-function

0 commit comments

Comments
 (0)