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 18, 2023
1 parent 0c22d89 commit dd1d4e7
Show file tree
Hide file tree
Showing 20 changed files with 147 additions and 72 deletions.
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add API for styles insertion",
"packageName": "@griffel/core",
"email": "olfedias@microsoft.com",
"dependentChangeType": "patch"
}
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: improve React 18 support",
"packageName": "@griffel/react",
"email": "olfedias@microsoft.com",
"dependentChangeType": "patch"
}
23 changes: 11 additions & 12 deletions packages/core/src/__resetStyles.ts
@@ -1,25 +1,24 @@
import { DEBUG_RESET_CLASSES } from './constants';
import { insertionFactory } from './insertionFactory';
import type { MakeResetStylesOptions } from './makeResetStyles';
import type { GriffelInsertionFactory } from './types';

/**
* @internal
*/
export function __resetStyles(ltrClassName: string, rtlClassName: string | null, cssRules: string[]) {
const insertionCache: Record<string, boolean> = {};
export function __resetStyles(
ltrClassName: string,
rtlClassName: string | null,
cssRules: string[],
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertStyles = factory();

function computeClassName(options: MakeResetStylesOptions): string {
const { dir, renderer } = options;
const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName;

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules({ r: cssRules! });
insertionCache[rendererId] = true;
}

const className = isLTR ? ltrClassName : rtlClassName || ltrClassName;
insertStyles(renderer, dir, { r: cssRules });

if (process.env.NODE_ENV !== 'production') {
DEBUG_RESET_CLASSES[className] = 1;
Expand Down
14 changes: 5 additions & 9 deletions packages/core/src/__styles.ts
@@ -1,6 +1,7 @@
import { debugData, isDevToolsEnabled, getSourceURLfromError } from './devtools';
import { insertionFactory } from './insertionFactory';
import { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots';
import type { CSSClassesMapBySlot, CSSRulesByBucket } from './types';
import type { CSSClassesMapBySlot, CSSRulesByBucket, GriffelInsertionFactory } from './types';
import type { MakeStylesOptions } from './makeStyles';

/**
Expand All @@ -11,8 +12,9 @@ import type { MakeStylesOptions } from './makeStyles';
export function __styles<Slots extends string>(
classesMapBySlot: CSSClassesMapBySlot<Slots>,
cssRules: CSSRulesByBucket,
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertionCache: Record<string, boolean> = {};
const insertStyles = factory();

let ltrClassNamesForSlots: Record<Slots, string> | null = null;
let rtlClassNamesForSlots: Record<Slots, string> | null = null;
Expand All @@ -24,10 +26,7 @@ export function __styles<Slots extends string>(

function computeClasses(options: Pick<MakeStylesOptions, 'dir' | 'renderer'>): Record<Slots, string> {
const { dir, renderer } = options;

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';

if (isLTR) {
if (ltrClassNamesForSlots === null) {
Expand All @@ -39,10 +38,7 @@ export function __styles<Slots extends string>(
}
}

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules(cssRules!);
insertionCache[rendererId] = true;
}
insertStyles(renderer, dir, cssRules);

const classNamesForSlots = isLTR
? (ltrClassNamesForSlots as Record<Slots, string>)
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Expand Up @@ -102,6 +102,7 @@ export {
StyleBucketName,
// Util
GriffelRenderer,
GriffelInsertionFactory,
} from './types';

// Private exports, are used by devtools
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/insertionFactory.ts
@@ -0,0 +1,12 @@
import { CSSRulesByBucket, GriffelInsertionFactory, GriffelRenderer } from './types';

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

return function insertStyles(renderer: GriffelRenderer, dir: string, cssRules: CSSRulesByBucket) {
if (insertionCache[renderer.id] === undefined) {
renderer.insertCSSRules(cssRules!);
insertionCache[renderer.id] = true;
}
};
};
17 changes: 6 additions & 11 deletions packages/core/src/makeResetStyles.ts
@@ -1,16 +1,18 @@
import type { GriffelResetStyle } from '@griffel/style-types';

import { DEBUG_RESET_CLASSES } from './constants';
import { insertionFactory } from './insertionFactory';
import { resolveResetStyleRules } from './runtime/resolveResetStyleRules';
import type { GriffelRenderer } from './types';
import type { GriffelInsertionFactory } from './types';

export interface MakeResetStylesOptions {
dir: 'ltr' | 'rtl';
renderer: GriffelRenderer;
}

export function makeResetStyles(styles: GriffelResetStyle) {
const insertionCache: Record<string, boolean> = {};
export function makeResetStyles(styles: GriffelResetStyle, factory: GriffelInsertionFactory = insertionFactory) {
const insertStyles = factory();

let ltrClassName: string | null = null;
let rtlClassName: string | null = null;
Expand All @@ -24,16 +26,9 @@ export function makeResetStyles(styles: GriffelResetStyle) {
[ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles);
}

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';
insertStyles(renderer, dir, { r: cssRules! });

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules({ r: cssRules! });
insertionCache[rendererId] = true;
}

const className = isLTR ? ltrClassName : rtlClassName || ltrClassName;
const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName;

if (process.env.NODE_ENV !== 'production') {
DEBUG_RESET_CLASSES[className] = 1;
Expand Down
30 changes: 14 additions & 16 deletions packages/core/src/makeStaticStyles.ts
@@ -1,31 +1,29 @@
import type { GriffelStaticStyles } from '@griffel/style-types';

import { insertionFactory } from './insertionFactory';
import { resolveStaticStyleRules } from './runtime/resolveStaticStyleRules';
import type { GriffelRenderer } from './types';
import type { GriffelInsertionFactory } from './types';

export interface MakeStaticStylesOptions {
renderer: GriffelRenderer;
}

/**
* Register static css.
* @param styles - styles object or string.
*/
export function makeStaticStyles(styles: GriffelStaticStyles | GriffelStaticStyles[]) {
const styleCache: Record<string, true> = {};
export function makeStaticStyles(
styles: GriffelStaticStyles | GriffelStaticStyles[],
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertStyles = factory();
const stylesSet: GriffelStaticStyles[] = Array.isArray(styles) ? styles : [styles];

function useStaticStyles(options: MakeStaticStylesOptions): void {
const { renderer } = options;
const cacheKey = renderer.id;

if (!styleCache[cacheKey]) {
renderer.insertCSSRules({
// 👇 static rules should be inserted into default bucket
d: resolveStaticStyleRules(stylesSet),
});
styleCache[cacheKey] = true;
}
insertStyles(
options.renderer,
// Static styles cannot be direction aware
'ltr',
// 👇 static rules should be inserted into default bucket
{ d: resolveStaticStyleRules(stylesSet) },
);
}

return useStaticStyles;
Expand Down
17 changes: 9 additions & 8 deletions packages/core/src/makeStyles.ts
@@ -1,15 +1,20 @@
import { debugData, isDevToolsEnabled, getSourceURLfromError } from './devtools';
import { insertionFactory } from './insertionFactory';
import { resolveStyleRulesForSlots } from './resolveStyleRulesForSlots';
import { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots';
import type { CSSClassesMapBySlot, CSSRulesByBucket, GriffelRenderer, StylesBySlots } from './types';
import type { GriffelInsertionFactory } from './types';

export interface MakeStylesOptions {
dir: 'ltr' | 'rtl';
renderer: GriffelRenderer;
}

export function makeStyles<Slots extends string | number>(stylesBySlots: StylesBySlots<Slots>) {
const insertionCache: Record<string, boolean> = {};
export function makeStyles<Slots extends string | number>(
stylesBySlots: StylesBySlots<Slots>,
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertStyles = factory();

let classesMapBySlot: CSSClassesMapBySlot<Slots> | null = null;
let cssRules: CSSRulesByBucket | null = null;
Expand All @@ -30,8 +35,6 @@ export function makeStyles<Slots extends string | number>(stylesBySlots: StylesB
}

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';

if (isLTR) {
if (ltrClassNamesForSlots === null) {
Expand All @@ -43,10 +46,8 @@ export function makeStyles<Slots extends string | number>(stylesBySlots: StylesB
}
}

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules(cssRules!);
insertionCache[rendererId] = true;
}
insertStyles(renderer, dir, cssRules!);

const classNamesForSlots = isLTR
? (ltrClassNamesForSlots as Record<Slots, string>)
: (rtlClassNamesForSlots as Record<Slots, string>);
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/types.ts
Expand Up @@ -87,6 +87,12 @@ export type CSSRulesByBucket = {
c?: CSSBucketEntry[];
};

export type GriffelInsertionFactory = () => (
renderer: GriffelRenderer,
dir: 'rtl' | 'ltr',
cssRules: CSSRulesByBucket,
) => void;

/** @internal */
export type CSSBucketEntry = string | [string, Record<string, unknown>];

Expand Down
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

0 comments on commit dd1d4e7

Please sign in to comment.