Emotion: A hook version of <ClassNames> for power users

Created on 25 Apr 2020  路  3Comments  路  Source: emotion-js/emotion

Preface: I know this topic has already been brought up a few times (#1295, #967, #1321) but I have a use-case that I haven't seen mentioned before, and a proposed solution that side-steps the inherent complexity of this problem, while giving power-users what they need.

The problem

  1. Our stack already uses a jsx pragma and so we cannot use Emotion's.
  2. At the same time we do server-side rendering with createCache. So we don't need (or want) the functionality of adding sibling <style> tags.
  3. We really need the ability to convert style objects to class names with just hooks, no render-prop components, because:

    1. we have hooks that compose hooks that compose hooks, and components have no place there.

    2. render-prop components mess with the es-lint hooks plugin when a hook is needed inside the render function.

The main issue as I understand it is that most people are going to need the version of Emotion that adds <style> tags next to the component that rendered them, so a hook is out of the question as it can't do that (at least not in a clean way). And exposing a hook that's only useful if you roll your own server-side rendering isn't ideal...

Proposed solution

So I propose that instead of Emotion supplying users with a potentially confusing hook API, just expose some of the functionality that would be needed to create it (the guts of ClassNames) and let us power-users roll our own. That way we're not leading less-informed users astray from the pit of success, but also letting more advanced users integrate Emotion into their existing stack without pain.

I'd be happy to take a stab at a PR that makes the minimal changes necessary to move this problem into 'user-land'. Is that something you'd be willing to consider merging?

feature request

Most helpful comment

Hi @oztune it might (or might not) be interesting to you, but I have written a hook that does this already. For my use-case server side rendering wasn't needed, but if you are able to get access to an EmotionCache on the server it might already work for what you are doing.

import { CSSInterpolation, serializeStyles } from '@emotion/serialize'
import { insertStyles } from '@emotion/utils'
import { useCallback } from 'react'

import { useEmotionCache } from 'ds/styled' // see below

/**
 * **This hook only works for browser rendering!** (I wrote it for separating
 * modal behaviour from modal styles).
 */
export function useCssClassName(): (...args: Array<CSSInterpolation>) => string {
  const cache = useEmotionCache()
  return useCallback(
    (...args) => {
      if (!cache) {
        if (process.env.NODE_ENV === 'production') {
          return 'emotion-cache-missing'
        }
        throw new Error('No emotion cache found!')
      }
      const serialized = serializeStyles(args, cache.registered)
      insertStyles(cache, serialized, false)
      return cache.key + '-' + serialized.name
    },
    [cache],
  )
}

The useEmotionCache hook is specific to our application, and it sounds like you're already in a position to implement your own version. Because I didn't need SSR for this hook, my version just exposes the default emotion cache in a custom context:

import React from 'react'
import { EmotionCache, withEmotionCache } from '@emotion/react'

const CacheContext = createContext<EmotionCache|undefined>(undefined)
export const useEmotionCache = () => useContext(CacheContext)

// We wrap our App with this
export const CacheProvider = withEmotionCache((
  { children }: { children: React.ReactNode },
  cache: EmotionCache
) => {
  return (
    <CacheContext.Provider value={cache}>
      {children}
   </CacheContext.Provider>
  )
})

All 3 comments

Hi @oztune it might (or might not) be interesting to you, but I have written a hook that does this already. For my use-case server side rendering wasn't needed, but if you are able to get access to an EmotionCache on the server it might already work for what you are doing.

import { CSSInterpolation, serializeStyles } from '@emotion/serialize'
import { insertStyles } from '@emotion/utils'
import { useCallback } from 'react'

import { useEmotionCache } from 'ds/styled' // see below

/**
 * **This hook only works for browser rendering!** (I wrote it for separating
 * modal behaviour from modal styles).
 */
export function useCssClassName(): (...args: Array<CSSInterpolation>) => string {
  const cache = useEmotionCache()
  return useCallback(
    (...args) => {
      if (!cache) {
        if (process.env.NODE_ENV === 'production') {
          return 'emotion-cache-missing'
        }
        throw new Error('No emotion cache found!')
      }
      const serialized = serializeStyles(args, cache.registered)
      insertStyles(cache, serialized, false)
      return cache.key + '-' + serialized.name
    },
    [cache],
  )
}

The useEmotionCache hook is specific to our application, and it sounds like you're already in a position to implement your own version. Because I didn't need SSR for this hook, my version just exposes the default emotion cache in a custom context:

import React from 'react'
import { EmotionCache, withEmotionCache } from '@emotion/react'

const CacheContext = createContext<EmotionCache|undefined>(undefined)
export const useEmotionCache = () => useContext(CacheContext)

// We wrap our App with this
export const CacheProvider = withEmotionCache((
  { children }: { children: React.ReactNode },
  cache: EmotionCache
) => {
  return (
    <CacheContext.Provider value={cache}>
      {children}
   </CacheContext.Provider>
  )
})

@grncdr Wow that's all I ever wanted, you're the man!

+1 this would be nice

Was this page helpful?
0 / 5 - 0 ratings