Relax the rules for emotion cache key name
The problem
I'm building a documentation site for our in-house UI library and one feature of the site is the ability to display live, editable examples with react-live within responsive viewports. This is simulated using iframes with the help of react-frame-component. In order to get these examples rendering correctly in iframes, I had to create separate instances of emotion's cache per viewport. Each of these instances needed a unique key. The challenge is that there aren't any ID generating libraries that can:
- be restricted to generating ids matching
/[^a-z-]/(source). The libraries I've come across generate alphanumeric or purely numeric ids - work in an isomorphic React app
Proposed solution
It would be great if we can revise the regular expression for the cache key to allow numbers in non-leading positions. This would allow me to use libraries like @reach/auto-id or @react-aria/utils (import {useId} from '@react-aria/utils').
I propose the cache key is validated using this regex:
/[a-z][a-z0-9-]*/
This makes it a lot easier to sandbox the emotion runtime inside an iframe:
import weakMemoize from "@emotion/weak-memoize";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { useId } from "@reach/auto-id";
import Frame, { FrameContextConsumer } from "react-frame-component";
export const LivePreview = (props) => {
// 👇👇👇
const cacheKeySuffix = useId();
const cacheKey = `sandbox-${cacheKeySuffix}`
const iframeInitialContent = initialContent(cacheKey);
// 👆👆👆
if (!cacheKeySuffix) {
return null;
}
return (
<Frame
title="Live demo sandbox"
initialContent={iframeInitialContent}
>
<FrameContextConsumer>
{({ document }) => (
<CacheProvider value={memoizedCreateCache(document.head)}>
{props.children}
</CacheProvider>
)}
</FrameContextConsumer>
</Frame>
);
};
const initialContent = (cacheKey) => `
<!DOCTYPE html>
<html>
<!-- 👇👇👇 STORE THE CACHE KEY HERE -->
<head data-emotion-cache-key="${cacheKey}">
<!-- 👆👆👆 -->
<title>Interactive code demo</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<div></div>
</body>
</html>
`;
const memoizedCreateCache = weakMemoize((container: HTMLElement) => {
return createCache({
container,
// 👇👇👇 PULL THE CACHE KEY HERE
key: container.getAttribute("data-emotion-cache-key") || "",
});
});
Alternative solutions
My current solution involved copying the source of @reach/auto-id and modifying to encode its numeric ID to the character range 'abcdefghij' then return that. At this point, I have a hook I can use to generate emotion cache keys.
Additional context
-
Allowed characters in identifiers: https://www.w3.org/TR/CSS21/syndata.html#characters
-
My
useAlphaIdvariation of theuseIdhook from@reach/auto-id
import * as React from "react";
import { useIsomorphicLayoutEffect } from "@reach/utils";
const ID_CHARACTERS = "abcdefghij";
let serverHandoffComplete = false;
let counter = 0;
const genId = () => {
const nextId = `${counter}`
.split("")
.map((digit) => {
const i = parseInt(digit, 10);
return ID_CHARACTERS[i];
})
.join("");
counter += 1;
return nextId;
};
/**
* useAlphaId
*
* This utility hook is used to generate a unique emotion cache key which needs
* to contain letters and dashes only.
*
* Note 1:
* This code is a modified version @reach/auto-id.
* See: https://github.com/reach/reach-ui/blob/develop/packages/auto-id/src/index.tsx
*
* Note 2:
* The returned ID will initially be `null` and will update after a component
* mounts. Users may need to supply their own ID if they need consistent values
* for SSR.
*
* @see Docs https://reach.tech/auto-id
*/
export default function useAlphaId(
idFromProps?: string | null
): string | undefined {
/*
* If this instance isn't part of the initial render, we don't have to do the
* double render/patch-up dance. We can just generate the ID and return it.
*/
const initialId = idFromProps || (serverHandoffComplete ? genId() : null);
const [id, setId] = React.useState(initialId);
useIsomorphicLayoutEffect(() => {
if (id === null) {
/*
* Patch the ID after render. We do this in `useLayoutEffect` to avoid any
* rendering flicker, though it'll make the first render slower (unlikely
* to matter, but you're welcome to measure your app and let us know if
* it's a problem).
*/
setId(genId());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (serverHandoffComplete === false) {
/*
* Flag all future uses of `useId` to skip the update dance. This is in
* `useEffect` because it goes after `useLayoutEffect`, ensuring we don't
* accidentally bail out of the patch-up dance prematurely.
*/
serverHandoffComplete = true;
}
}, []);
return id != null ? String(id) : undefined;
}
I wouldn't be opposed to it but:
- a PR for this would have to be provided by you or somebody else from the community
- we'd have to make sure that this doesn't break SSR anyhow (for example some regexps in there might become incorrect after the proposed change)