Upgrading styled-components from v3 to v4
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 oldinjectGlobal
- 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 unifiedstyled(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 moreinnerRef
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!