Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: improve React 18 support by using useInsertionEffect",
"packageName": "@griffel/react",
"email": "olfedias@microsoft.com",
"dependentChangeType": "patch"
}
11 changes: 3 additions & 8 deletions packages/react/src/RendererContext.tsx
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/__resetStyles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { __resetStyles as vanillaResetStyles } from '@griffel/core';
import type { CSSRulesByBucket } from '@griffel/core';

import { insertionFactory } from './insertionFactory';
import { useRenderer } from './RendererContext';
import { useTextDirection } from './TextDirectionContext';

Expand All @@ -15,7 +16,7 @@ export function __resetStyles(
rtlClassName: string | null,
cssRules: CSSRulesByBucket | string[],
) {
const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules);
const getStyles = vanillaResetStyles(ltrClassName, rtlClassName, cssRules, insertionFactory);

return function useClasses(): string {
const dir = useTextDirection();
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/__styles.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,7 +15,7 @@ export function __styles<Slots extends string>(
classesMapBySlot: CSSClassesMapBySlot<Slots>,
cssRules: CSSRulesByBucket,
) {
const getStyles = vanillaStyles(classesMapBySlot, cssRules);
const getStyles = vanillaStyles(classesMapBySlot, cssRules, insertionFactory);

return function useClasses(): Record<Slots, string> {
const dir = useTextDirection();
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/createDOMRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof React.useInsertionEffect>;

describe('createDOMRenderer', () => {
it('rehydrateCache() avoids double insertion', () => {
Expand Down Expand Up @@ -46,13 +53,20 @@ 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(
<RendererProvider renderer={serverRenderer}>
<ExampleComponent />
</RendererProvider>,
);
const stylesHTML = renderToStaticMarkup(<>{renderToStyleElements(serverRenderer)}</>);

useInsertionEffect.mockImplementation(React.useInsertionEffect);

// Ensure that all styles are inserted into the cache
expect(serverRenderer.insertionCache).toMatchInlineSnapshot(`
Object {
Expand Down
24 changes: 24 additions & 0 deletions packages/react/src/insertionFactory-node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* @jest-environment node
*/

// 👆 this is intentionally to test in SSR like environment

import type { GriffelRenderer } from '@griffel/core';
import * as React from 'react';

import { insertionFactory } from './insertionFactory';

describe('insertionFactory (node)', () => {
it('does not use insertionEffect', () => {
const useInsertionEffect = jest.spyOn(React, 'useInsertionEffect');

const renderer: Partial<GriffelRenderer> = { id: 'a', insertCSSRules: jest.fn() };
const insertStyles = insertionFactory();

insertStyles(renderer as GriffelRenderer, { d: ['a'] });

expect(useInsertionEffect).not.toHaveBeenCalled();
expect(renderer.insertCSSRules).toHaveBeenCalledTimes(1);
});
});
27 changes: 27 additions & 0 deletions packages/react/src/insertionFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { GriffelRenderer } from '@griffel/core';

import { insertionFactory } from './insertionFactory';
import { useInsertionEffect as _useInsertionEffect } from './useInsertionEffect';
import * as React from 'react';

jest.mock('./useInsertionEffect', () => ({
useInsertionEffect: jest.fn().mockImplementation(fn => fn()),
}));

const useInsertionEffect = _useInsertionEffect as jest.MockedFunction<typeof React.useInsertionEffect>;

describe('canUseDOM', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('uses "useInsertionEffect" when available', () => {
const renderer: Partial<GriffelRenderer> = { insertCSSRules: jest.fn() };
const insertStyles = insertionFactory();

insertStyles(renderer as GriffelRenderer, { d: ['a'] });

expect(useInsertionEffect).toHaveBeenCalledTimes(1);
expect(renderer.insertCSSRules).toHaveBeenCalledTimes(1);
});
});
25 changes: 25 additions & 0 deletions packages/react/src/insertionFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { CSSRulesByBucket, GriffelInsertionFactory, GriffelRenderer } from '@griffel/core';

import { canUseDOM } from './utils/canUseDOM';
import { useInsertionEffect } from './useInsertionEffect';

export const insertionFactory: GriffelInsertionFactory = () => {
const insertionCache: Record<string, boolean> = {};

return function insert(renderer: GriffelRenderer, cssRules: CSSRulesByBucket) {
// Even if `useInsertionEffect` is available, we can use it on a client only as it will not be executed in SSR
if (useInsertionEffect && 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;
}
};
};
5 changes: 3 additions & 2 deletions packages/react/src/makeResetStyles.ts
Original file line number Diff line number Diff line change
@@ -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()) {
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/makeStaticStyles.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/makeStyles.ts
Original file line number Diff line number Diff line change
@@ -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<Slots extends string | number>(stylesBySlots: Record<Slots, GriffelStyle>) {
const getStyles = vanillaMakeStyles(stylesBySlots);
const getStyles = vanillaMakeStyles(stylesBySlots, insertionFactory);

if (process.env.NODE_ENV !== 'production') {
if (isInsideComponent()) {
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/useInsertionEffect.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions packages/react/src/utils/canUseDOM-node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* @jest-environment node
*/

// 👆 this is intentionally to test in SSR like environment

import { canUseDOM } from './canUseDOM';

describe('canUseDOM (node)', () => {
it('returns "false"', () => {
expect(canUseDOM()).toBe(false);
});
});
7 changes: 7 additions & 0 deletions packages/react/src/utils/canUseDOM.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { canUseDOM } from './canUseDOM';

describe('canUseDOM', () => {
it('returns "true"', () => {
expect(canUseDOM()).toBe(true);
});
});
6 changes: 6 additions & 0 deletions packages/react/src/utils/canUseDOM.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Verifies if an application can use DOM.
*/
export function canUseDOM(): boolean {
return typeof window !== 'undefined' && !!(window.document && window.document.createElement);
}