@compiled/react
Version:
A familiar and performant compile time CSS-in-JS library for React.
114 lines (105 loc) • 3.97 kB
text/typescript
import React, { type ReactElement } from 'react';
import { isServerEnvironment } from './is-server-environment.js';
export interface StyleContainerConfig {
/**
* The DOM node into which Compiled will insert `<style>` elements.
*/
container: HTMLElement | ShadowRoot;
/**
* A unique key used to namespace the deduplication cache for this container.
* Styles inserted into this container are tracked separately from the main
* document cache, preventing cross-container cache collisions.
*
* Choose a key that is unique per container instance (e.g. `"shadow-toolbar"`).
*/
cacheKey: string;
}
/**
* Singleton holding the currently active style container config on the client.
* Read directly by useStyleContainer().
*/
export let clientStyleContainer: StyleContainerConfig | null = null;
/**
* Returns the currently active style container config.
*/
export const useStyleContainer = (): StyleContainerConfig | null => {
return clientStyleContainer;
};
/**
* Provides a custom DOM container for Compiled style injection within a React subtree.
*
* **Runtime mode only.** This provider is not supported in server environments or when
* using CSS extraction (`@compiled/babel-plugin-strip-runtime`). In extraction mode,
* `CS`/`CC` components are removed at build time, so there is nothing for this provider
* to intercept. Supporting Shadow DOM with extraction is a known limitation and is
* planned as future work.
*
* Use this when rendering Compiled components inside a Shadow DOM, where styles
* inserted into the main document `<head>` are not visible to the shadow tree.
*
* The `cacheKey` must be unique per container and is used to namespace the
* deduplication cache so styles are tracked independently per container.
*
* @example
* ```tsx
* import { StyleContainerProvider } from '@compiled/react';
* import { createPortal } from 'react-dom';
*
* function ShadowHost() {
* const hostRef = useRef<HTMLDivElement>(null);
* const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
*
* useEffect(() => {
* if (hostRef.current && !hostRef.current.shadowRoot) {
* setShadowRoot(hostRef.current.attachShadow({ mode: 'open' }));
* }
* }, []);
*
* return (
* <div ref={hostRef}>
* {shadowRoot &&
* createPortal(
* <StyleContainerProvider container={shadowRoot} cacheKey="my-shadow-root">
* <MyCompiledComponent />
* </StyleContainerProvider>,
* shadowRoot
* )}
* </div>
* );
* }
* ```
*/
export function StyleContainerProvider({
container,
cacheKey,
children,
}: StyleContainerConfig & { children: React.ReactNode }): ReactElement {
if (isServerEnvironment()) {
if (process.env.NODE_ENV === 'development') {
console.warn(
'@compiled/react: StyleContainerProvider has no effect in server environments. ' +
'Shadow DOM content should be rendered client-side only (e.g. inside a portal ' +
'guarded by useEffect/useState).'
);
}
return children as ReactElement;
}
// On the client, set the singleton synchronously during the render phase via
// useMemo so CS children pick it up immediately.
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useMemo(() => {
clientStyleContainer = { container, cacheKey };
}, [container, cacheKey]);
// Clear the singleton when this provider unmounts.
// Note: nested StyleContainerProviders are not supported. If an inner provider
// unmounts, this will clear clientStyleContainer to null rather than restoring
// the outer provider's value. This is an acceptable limitation for the intended
// use case of a single provider wrapping a shadow DOM subtree.
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
return () => {
clientStyleContainer = null;
};
}, []);
return children as ReactElement;
}