The v4 release of styled-components comes with a lot of features and performance wins that we were excited about. Here are the release highlights:

  • Smaller and much faster, going from 16.1kB to less than 15kB, and speeding up mounting by ~25% and re-rendering by ~7.5%
  • A brand new createGlobalStyle API, the hot-reloadable and themable replacement for the old injectGlobal
  • Support for the "as" prop, a more flexible alternative to .withComponent()
  • Removal of Comp.extend, with an automatic codemod to move your entire codebase to the unified styled(Comp) notation
  • Full StrictMode compliance for React v16, which also means we had to drop support for React v15 and lower (you may be able to use polyfills to get v15 working with styled-components v4)
  • Native support for ref on any styled component, no more innerRef thanks to React v16

If you're like me, you might look at the official v3 to v4 migration guide, see the codemod, and feel pretty confident that the migration should take you less than a day! Welp, I was wrong. Here are a few gotchas I ran into:

Gotcha #1: Replacing the deprecated YourComp.extend with styled(YourComp) and how that affects .withComponent

When you run the codemod, it replaces all of the deprecated YourComp.extend code with styled(YourComp). If you use .withComponent on something created with .extend before the codemod, it won't work as intended. For example:

import styled from 'styled-components'
import { space, width } from 'styled-system'

const Base = styled('div')`
  ${space}
  ${width}
`

const Heading = Base.extend`
  font-weight: bold;
  border-bottom: 2px solid #333;
`

const Title = Heading.withComponent('h1')

In v3, Title would get the styles from Heading and Base, then render as an h1 element. 🆒

After running the codemod, you'd get:

...

const Heading = styled(Base)`
  font-weight: bold;
  border-bottom: 1px solid black;
`

const Title = Heading.withComponent('h1')

That .withComponent will no longer behave as intended. Rather than Heading wrapping Base, Heading will now only wrap the h1. You won't get any of the styles from Base.

Instead, use attrs and the new as prop. This will properly merge all the styles together and only swap out what gets rendered:

const Title = styled(Heading).attrs(() => ({ as: 'h1' }))``

Gotcha #2: .attrs evaluation order

If you have multiple components folded together using styled(), be aware that the order in which attrs are evaluated starts from the base component outwards.

For example, in v3 we had a base Icon component that relies on a name prop to add the desired svg path as children:

const Icon = styled('svg').attrs({
  children: ({ name }) => <path d={ICONS[name].path} /> 
})``

Elsewhere we had an EditIcon that extended the Icon and provided that name prop:

const EditIcon = Icon.extend.attrs({
  name: 'pencil'
})``

This worked in v3, but after swapping out that .extend for styled(), we got a runtime exception when the EditIcon is rendered.

const Icon = styled('svg').attrs({
  children: ({ name }) => <path d={ICONS[name].path} /> // EXCEPTION THROWN HERE
})``

const EditIcon = styled(Icon).attrs({
  name: 'pencil' 
})``

We got "Cannot read property path of undefined" because the attrs for Icon are evaluated before the attrs for EditIcon are provided, and the name prop is undefined at that point. The fix here was to use defaultProps which makes that name available to the base Icon component sooner.

const EditIcon = styled(Icon)``
EditIcon.defaultProps = {
  name: 'pencil'
}

Gotcha #3: Casing of css attrs

In v4, we ended up with css values with px units added where we didn't expect. If you're returning an object with css rules, make sure the keys are camel-cased and not dash-cased. In v4, the detection of 'unit-less' rules assumes you're providing camel-cased keys:

In v3 we had a shorthand helper returning an object with dash-cased css rules, e.g., something like:

const shortHand = props => {
  return {
    'line-height': props.lh
  }
}

const Base = styled('div')`
  ${shortHand}
`

...

<Base lh={1.4}></Base>

In v3, the resulting css was: line-height: 1.4 (expected) In v4, the resulting css was: line-height: 1.4px (unexpected)

The solution is to return camel-cased rules:

const shortHand = props => {
  return {
    lineHeight: props.lh
  }
}

That's it!

With the official v3 to v4 migration guide and these few gotchas in mind, the upgrade should be relatively quick and straight forward. The smaller bundle size, faster performance, and improved API of v4 are worth the effort!