question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Hashed className key collisions

See original GitHub issue

Current behavior:

We encountered an issue recently where a single page of a moderately sized blog was rendering a component incorrectly. A quick investigation discovered that the generated class name assigned (css-r1ywwf) was identical to another, correctly-styled component on the page, and the styles associated with the duplicate class were consistent with the correctly-styled component.

This led to some spelunking into how Emotion generates the hash, which lead me to the @emotion/hash package, which has a nearly unintelligible (bit shifting, hexadecimal literals, and single-character variables oh my!) hashing function I hadn’t heard of, so I did what I get paid to do… I Google’d it, which led me to a write-up by the author of MurmurHash that discusses a flaw that can result in the algorithm having a significantly reduced potential output pool. This leads to increased collisions (“a better than 50% chance of finding a collision in a group of 13115”).

Okay, so under the assumption that this is the source of the class-name collision that we are experiencing, I went to try to pull some more details from the buggy page to see if I can create a reproduction, and that’s when I discovered the registered and inserted lists in the Emotion Context, which I presume are handling collisions. And this is also when I notice that the div that is in my DOM with the incorrect class name is actually being passed the correct className prop, which is generated by a pretty vanilla css prop if I use React Devtools to inspect it. So the actual DOM node seems to disagree with React Devtools.

It gets weirder, though. I’m using Gatsby, and I noticed that the issue doesn’t present itself if you navigate to the page from another page on the site, only if you load the page directly. I assumed this would be related to server-side rendering, so I checked the static HTML and the correct class and styles are used, so the incorrect class is being assigned as part the rehydration mount, not during SSR or during client-side render from raw data.

Short summary:

  • A single Emotion-generated class name is being applied to multiple components that are not using the same styles
  • The correct class name is passed to the React element, but the incorrect value is rendered to the DOM
  • The issue appears to only present when rehydrating server-side rendered content
  • I’m stumped, but anticipate someone with more knowledge of the Emotion internals can pinpoint the issue pretty quickly.
  • 😬🙏

Environment information:

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:3
  • Comments:21 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
hawkettcommented, Apr 27, 2020

re. demo repository, unfortunately I don’t have time at the moment. Hopefully the information provided is more useful than not. It sounds like you’re saying that collision handling won’t be implemented in any case - what next steps do you see if two colliding strings were identified in an algorithm known to have collisions?

re. collision checking, it seems the consequences of a collision (css silently applied incorrectly), would be easily more important than any performance hit. P1: do it right, P2: do it fast. The argument that ‘everyone implements it like that’ doesn’t seem convincing.

re. performance, apart from a map lookup, the collision handling code path should only be incurred if there is actually a collision, which (as you note) should be rare. At first glance it seems the performance hit would be minimal, but perhaps I have misunderstood the problem. Even a warning that a collision did occur would be valuable, regardless of whether it was actually resolved by Emotion. It could then be ignored or fixed (with a custom label) at the user’s discretion. Warning for conflicts could be an optional configuration so there is no performance hit at all, turned on in development and off in production.

re. autoLabel demo - unfortunately not at the moment, but hopefully the information is of some value.

re. Babel - I’m using the Gatsby default config. Is there a specific configuration you think might be at fault here?

1reaction
hawkettcommented, Apr 27, 2020

We’ve also encountered this problem in a large(ish) codebase. I haven’t been able to reproduce in a small demo repository, but have some details of the implementation, and what we’ve done to fix.

We define a component that looks similar to this:

export const Glyph = styled(BaseGlyph)<{ name: string }>`
  &::after {
    content: '${props => getChar(props.name)}';
  }
`;

There’s ~200 variations of name that produce different glyphs from a custom font (where characters like ‘a’, ‘b’ etc. render different glyphs) and name is a human readable form that maps to the font character in getChar. It is used like <Glyph name="MeaningfulSymbolName" />. BaseGlyph defines a few styles like font and color, but is not especially complicated.

The reason that we do it this way (rather than rendering the character directly) is that we previously did not have a font for these glyphs and rendered them as separate svg, so they had React components that matched the same component <Glyph name="..." /> signature. This method using css content allowed us to switch to an emotion-only font implementation without changing the component API. There are certainly other possible implementations that may not have resulted in a name collision.

The result of this implementation is needing ~200 different hashes for styles - each style string maybe one or two hundred characters long and of the same length, but varying by a single character.

We render many thousands of Glyphs in a single page, and the result of the issue is that one (or more) of the glyphs are consistently replaced by some other glyph because of the name collision. The easiest way to conceptualize the impact is to imagine we are rendering a long and complex page of mathematical formula, with the occasional symbol substituted for some other arbitrary symbol.

The issue was resolved in this instance by supplying a custom label name:

export const Glyph = styled(BaseGlyph)<{ name: string }>`
  &::after {
    content: '${props => getChar(props.name)}';
  }
  ${props => `label:Glyph-${props.name}`};
`;

this generated: css-ppg6yw-Glyph-SymbolName1 & css-c954va-Glyph-SymbolName2, whereas both were generated as css-112tzn2 previously. As a side note, maybe related, we do have autoLabel enabled, but for about 80% of our components the component name is not appended (as in this example).

The following also caused emotion to generate distinct class names:

export const Glyph = styled(BaseGlyph)<{ name: string }>`
  &::after {
    content: '${props => getChar(props.name)}';
  }
  /* ${props => props.name} */
`;

So just making the css string vary on name works in this particular instance, but I’m guessing that this just changes the potential for collision, whereas the label solution removes any possibility.

A couple of questions:

  • I haven’t had a chance to review whether Emotion does attempt to handle collisions as surmised by the OP - do you see this collision as a bug, or should the expectation be that css names might collide on occasion?
  • If we should expect the occasional collision pretty much anywhere in our document, is it advised to ensure no collision by adding a custom label to every component, composing the values of each component parameter?
Read more comments on GitHub >

github_iconTop Results From Across the Web

Solve collision className of css modules in react-create-app ...
If you're using the latest version of react you can do these: Rename the CSS file to some.module.css (assuming it was some.css before)....
Read more >
Building Java Programs - Washington
— As with any general hashing function, collisions are possible. — Example: "Ea" and "FB" have the same hash value. — Early versions...
Read more >
Top 5 @emotion/hash Code Examples - Snyk
Learn more about how to use @emotion/hash, based on @emotion/hash code examples ... which can result in class name collisions */ } else...
Read more >
How to build a hash generator application with React
Build a React application that will hash both text and files in this step-by-step tutorial and example build.
Read more >
Using CSS Modules in React - OpenReplay Blog
... global scope and collisions by producing a random string as a className name and adding a unique hash to make each className...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found