Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: improve React 18 support #72

Merged
merged 2 commits into from Jul 31, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
/**
* Verifies if an application can use DOM.
*/
export function canUseDOM(): boolean {
return typeof window !== 'undefined' && !!(window.document && window.document.createElement);
}