emotion-js / emotion

👩‍🎤 CSS-in-JS library designed for high performance style composition
https://emotion.sh/
MIT License
17.53k stars 1.11k forks source link

Can't clear emotion cache. #3133

Open mctrafik opened 1 year ago

mctrafik commented 1 year ago

Current behavior:

injectGlobal from @emotion/css retains injections from another emotion server!

To reproduce:

It's a server-side rendered app.

Render code:

import { getMarkupFromTree } from '@apollo/client/react/ssr';
import { cache as emotionCache, injectGlobal } from '@emotion/css';
import { CacheProvider as EmotionCacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import type { NextFunction, Request, Response } from 'express';
import type { FC } from 'react';
import { renderToString } from 'react-dom/server';

const App: FC<{ bgColor: string }> = ({ bgColor }) => {
  injectGlobal(`body { background-color: ${bgColor}; }`);
  return <>Dummy App</>;
};

export async function renderDummy(
  request: Request,
  response: Response,
  next: NextFunction
): Promise<void> {
  const currentUrl = new URL(request.url, `${request.protocol}://${request.hostname}`);
  const bgColor = currentUrl.searchParams.get('background-color') ?? 'blue';

  const { extractCriticalToChunks, constructStyleTagsFromChunks } =
    createEmotionServer(emotionCache);

  const reactMarkup = (
    <EmotionCacheProvider value={emotionCache}>
      <App bgColor={bgColor} />
    </EmotionCacheProvider>
  );

  const markup = await getMarkupFromTree({
    tree: reactMarkup,
    renderFunction: renderToString,
  });

  const styleChunks = extractCriticalToChunks(markup);
  const styles = styleChunks.styles.map(style => {
    return { ...style, key: 'my-app' };
  });

  const emotionStyleTags = constructStyleTagsFromChunks({
    html: markup,
    styles,
  });

  const output = `<!DOCTYPE html> 
<head>
${emotionStyleTags}
</head>
<html>
<body>
${markup}
</body>
</html>
  `;

  response.setHeader('Content-Type', 'text/html');
  response.write(output);
  response.status(200);
  response.end();
}

Calling http://localhost will render text on blue background (because that's the default). Calling http://localhost/?background-color=yellow will render a yellow background Calling http://localhost again will keep rendering yellow background even though it should be blue!

Expected behavior:

It should render the background compatible with the the current invocation, and not use state cache state for global injection.

Note that I tried to make a new cache every render, i.e. const emotionCache = createEmotionCache({ key: 'marketing-web-emotion' }); but that just doesn't work. No warnings, errors, nor content.

If nothing else, it would be nice to be able to reset the cache, but that also doesn't seem possible. I tried inspecting cache's inserted and registered properties, but they don't contain globally injected styles.

Environment information:

mctrafik commented 1 year ago

This is the other example I mention where I create a new cache:

import { getMarkupFromTree } from '@apollo/client/react/ssr';
import createEmotionCache from '@emotion/cache';
import { injectGlobal } from '@emotion/css';
import { CacheProvider as EmotionCacheProvider } from '@emotion/react';
import { extractCritical } from '@emotion/server';
import type { NextFunction, Request, Response } from 'express';
import type { FC } from 'react';
import { renderToString } from 'react-dom/server';

const App: FC<{ bgColor: string }> = ({ bgColor }) => {
  injectGlobal(`body { background-color: ${bgColor}; }`);
  return <>Dummy App</>;
};

/** For investigation of https://github.com/emotion-js/emotion/issues/3133 */
export async function renderDummy2(
  request: Request,
  response: Response,
  next: NextFunction
): Promise<void> {
  const currentUrl = new URL(request.url, `${request.protocol}://${request.hostname}`);
  const bgColor = currentUrl.searchParams.get('background-color') ?? 'blue';

  const emotionKey = 'my-emotion';

  const emotionCache = createEmotionCache({ key: emotionKey });

  const reactMarkup = (
    <EmotionCacheProvider value={emotionCache}>
      <App bgColor={bgColor} />
    </EmotionCacheProvider>
  );

  const markup = await getMarkupFromTree({
    tree: reactMarkup,
    renderFunction: renderToString,
  });

  const { ids, css } = extractCritical(markup);
  const emotionStyleTags = `<style data-emotion="${emotionKey} ${ids.join(' ')}">${css}</style>`;

  const output = `<!DOCTYPE html> 
<head>
${emotionStyleTags}
</head>
<html>
<body>
${markup}
</body>
</html>
  `;

  response.setHeader('Content-Type', 'text/html');
  response.write(output);
  response.status(200);
  response.end();
}

Some more context: we have to use @emotion/css because we depend on a few third party packages that use it.

Andarist commented 1 year ago

If you want me to take a look at this please prepare this in a runnable form. I need to be able to jump into debugging this quickly - otherwise, I simply won't be able to afford the time spent on setting this situation up.

Note that I tried to make a new cache every render, i.e. const emotionCache = createEmotionCache({ key: 'marketing-web-emotion' }); but that just doesn't work. No warnings, errors, nor content.

This sounds like you are on the right track. I don't know though why it doesn't work.

mctrafik commented 1 year ago

Your response doesn't read very friendly. I am reporting a bug. If you want to help me, provide instructions on what you consider runnable. Do you have a server side rendering codepen? Emotion documentation is very broken when it comes to SSR. You shouldn't blame the users.

Andarist commented 1 year ago

Your response doesn't read very friendly.

That certainly wasn't my intention.

I am reporting a bug. If you want to help me, provide instructions on what you consider runnable.

A CodeSandbox/StackBlitz or a repository that I could clone.

Emotion documentation is very broken when it comes to SSR. You shouldn't blame the users.

You can't blame a free laborer either 🤷‍♂️ I'm willing to help you out - even though I don't have to since I don't have any obligation to do so. I like helping people though but I just can't afford to spend more time on it than necessary. That's a perhaps harsh truth but this is just a side project that I maintain and I have a day job and a family to take care of on top of that.

mctrafik commented 1 year ago

I do understand that there are other circumstances and we aren't all unemployed, rich people with nothing but time on our hands. I have a job and a family, too. And I was taken from them by dealing with an oncall issue with the visuals on our production site getting corrupted. After troubleshooting for a few hours, it boiled down to me filing this bug, and it was already 1am my time. So please understand that you're not the only one who's frustrated here.

That being said here's a blitz for you to fork: https://stackblitz.com/edit/node-z8fb7h?file=emotionBug3133.tsx which shows the issue that I can't find a solution to.

Code copied here for posterity, and so that other can find this by searching:

import createEmotionCache from '@emotion/cache';
import { injectGlobal } from '@emotion/css';
import { CacheProvider as EmotionCacheProvider } from '@emotion/react';
import { extractCritical } from '@emotion/server';
import type { FC } from 'react';
import { renderToString } from 'react-dom/server';

const App: FC<{ bgColor: string }> = ({ bgColor }) => {
  injectGlobal(`body { background-color: ${bgColor}; }`);
  return <>Dummy App</>;
};

function renderStyle(color: string): string {
  const emotionKey = 'my-emotion';

  const emotionCache = createEmotionCache({ key: emotionKey });

  const reactMarkup = (
    <EmotionCacheProvider value={emotionCache}>
      <App bgColor={color} />
    </EmotionCacheProvider>
  );

  const markup = renderToString(reactMarkup);

  const { ids: _ids, css } = extractCritical(markup);

  return css;
}

/** For investigation of https://github.com/emotion-js/emotion/issues/3133 
 * 
 * Should print:
 * 
 *  results: [
    'body{background-color:blue;}',
    'body{background-color:green;}',
    'body{background-color:blue;}'
  ]
 * 
 * But prints:
 * 
 *  results: [
    'body{background-color:blue;}',
    'body{background-color:blue;}body{background-color:green;}',
    'body{background-color:blue;}body{background-color:green;}'
  ]
 * 
*/
function main(): void {
  const results = [renderStyle('blue'), renderStyle('green'), renderStyle('blue')];

  console.info({ results });
}

try {
  main();
} catch (error) {
  console.error(error);
}

Note that my comment about the documentation being bad, is that following it literally has no results. It doesn't work, as described in this bug: https://github.com/emotion-js/emotion/issues/2731. I'm already very happy that I'm able to use emotion SSR for a happy path.

Andarist commented 1 year ago

I'm not really frustrated. At this point, I'm kinda just used to getting reports of this nature and I'm fine with it. It's just that to be able to assist with your problems (related to your job), I need to get that runnable repro case. Without it, I just can't justify spending time on an issue like this.

While my statement might sound harsh or something - please bear in mind that that's truly not my intention at all. I'm just static facts from my PoV. I'm also not a native speaker so my written language might sound more rough on the edges than it is supposed to.


That being said... now I see the problem. You can't use injectGlobal like this and the same applies to extractCritical. Both of those are bound to a specific cache - the default one. So accidentally you keep using the very same cache (the default one) instead of the one that you create in renderStyle. If you want to use those particular APIs you need to use @emotion/css/create-instance and @emotion/server/create-instance.

It might also just be easier to use <Global/> from @emotion/react instead of injectGlobal. This should automatically integrate with the cache that you are propagating through the React context.

However, you mentioned this:

Some more context: we have to use @emotion/css because we depend on a few third party packages that use it.

And that's something I don't understand - or rather I don't understand why you have to do it. So if the suggestions above don't help you then please tell me more about this requirement.

it would be nice to be able to reset the cache, but that also doesn't seem possible

You probably should be able to do this on import { cache } from '@emotion/css'. I don't really recommend this though - once cleared the cache should be disposed and you only want to clear it in order to recycle it and I can't guarantee that you won't find any issues with that.

mctrafik commented 1 year ago

I see. I can't control the library's usage of injectGlobal. But I am curious on how to clear global cache from import { cache } from '@emotion/css'. I haven't been able to find a way.

For context: my team owns a website that uses a component library that styles components using functions from @emotion/css, css and injectGlobal. Our app is server side rendered, and the component library can be rendered in either dark or light themes. I added the code to be able to switch in which mode the app is rendered (was always light before, now can be either light or dark). But now it's always dark the second anyone requests a dark page.

mctrafik commented 1 year ago

For others finding this thread, Here's what I'm doing to clear globally injected css from the cache:

import { cache as emotionCache } from '@emotion/css';

  for (const hash in emotionCache.inserted) {
    const value = emotionCache.inserted[hash]!;

    if (typeof value === 'boolean') continue;
    // Skip any keyframe values because they look like they were injected globally
    // even if they weren't.
    if (value.includes('@keyframes')) continue;

    // Skip values added through `css`. Those will always reference the hash.
    if (value.includes(hash)) continue;

    delete emotionCache.inserted[hash];
  }

but it does feel hacky and likely to break in future versions of emotion imho. Would love to know if there's a more robust way.

Edit: I ended up not wiping all injectGlobal styles, but instead keeping track of keys of cache.inserted and deleting only the new ones added after a render cycle.

torressam333 commented 2 months ago

Does emotion cache not have an auto clearing/cleaning mechanism in place? Does it forever append styles into the ?