Skip to content

Commit

Permalink
feat: improve React 18 support
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Jul 25, 2023
1 parent 9f7710e commit ab3c8ed
Show file tree
Hide file tree
Showing 11 changed files with 76 additions and 16 deletions.
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: improve React 18 support",
"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,5 +1,6 @@
import { __resetStyles as vanillaResetStyles } from '@griffel/core';

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

Expand All @@ -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();
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
27 changes: 27 additions & 0 deletions 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<string, boolean> = {};

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;
}
};
};
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;
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);
}

0 comments on commit ab3c8ed

Please sign in to comment.