Styled-system: Most performant way to write styled-system components

Created on 3 Jun 2019  ยท  14Comments  ยท  Source: styled-system/styled-system

Just a heads up - Asking existential questions, no bug report following

With v5 comes a few interesting changes to the styled-system API. As I make my way through the source code, I still can't quite figure out the best way to write a component using styled-system. I had the same issue with styled-system 4 when writing my most recent UI library based on styled-system. While I understand there are many different ways to write the same component (it appears you've left it intentionally open-ended, which I appreciate), I'm concerned primarily about performance and bundle size. I would love any pointers in the appropriate direction as possible.

Assuming I have a base Box component (similar or identical to the one in Rebass) that I want to extend in some way, I could do any of the following:

1. Extend a component via css

render() {
  <div>
    <Box {...awesomeProps} p={2} css={css({ /* Some cool styles in here, totes responsive n' all */ })}>
      Aww yeah...
    </Box>
  </div>
}

2. Create a new component via styled

const SpecialBox = styled(Box)(props =>({
  // My awesome styles go here
  // Maybe even use "themeGet" if I'm feeling lucky
  [themeGet('media.medium')(props)]: {
    // Responsive styles are super awkward though and don't get to use the array
  }
}),
// Things I want to override go here
color,
space);

SpecialBox.defaultProps = {
  // Some cool defaults go here
};

3. Create a new component via styled and css

const SpecialBox = styled(Box)(
  css({
    // Basically the same as before, but without awkward themeGet and with arrays allowed
  })
),
// Things I want to override go here
color,
space);

SpecialBox.defaultProps = {
  // Some cool defaults go here
};

4. Write in the style prop on the component

render() {
  <div>
    <Box {...awesomeProps} p={2} style={{ /* Some cool styles in here, no pseudo-classes or responsiveness */ }}>
      Aww yeah...
    </Box>
  </div>
}

Likewise, if I want to create a component with identical styles as Box, but with a different name (for readability sake) then I could do something like:

const MyList = styled(Box)({});

export default props => (
  <MyList>
    <MyListItem>Hello 1</MyListItem>
    <MyListItem>Hello 2</MyListItem>
    <MyListItem>Hello 3</MyListItem>
  </MyList>
);
  • What are the performance implications of any of these methods of creating a component?
  • Are there any differences in the generated bundle size between the methods?
  • Why even offer themeGet() as a method of getting theme values if it's way longer and does the same functionality as css() but without responsive arrays? This method seems pretty unuseful, although I admit to using it a lot when I first started on styled-system because I didn't know better. Are fallback values the only thing I'm given from themeGet() that I don't get with css()?
  • It appears as though you're rewriting Rebass using styled-system v5 using method 2 from above. Of course, Rebass is far simpler of a library than most websites will use in practice (since it's entirely intended to be a "building block" library from which you can build more complex components). How do you write more complex components in the real-world with respects to performance and generated bundle size? Of course, no disrespect intended with Rebass - it's a terrific micro-library for creating components.
  • I noticed that you mention in the v5 migration guide that we should now be compose-ing all of our style functions together for better performance.
    > When using multiple categories or style functions together, use the compose utility before passing the functions to the styled higher order component. This will help ensure the best performance possible.

I don't have a question on the above point, but was hoping you could elaborate.

Perhaps I'm a bit more partial to being instructed on the "best practices" of using a component library. However, I also know there's not a "one size fits all" answer for any of these questions.
I was curious if you or anyone else have any real-world examples of where one method of creating components has been better (read "more maintainable", "cleaner", or "more performant") than another. Perhaps I'm asking in the wrong project as well, and should instead pose this to the styled-components or emotion community instead.

Thanks for everything, I love what you're doing on this project - I'm looking to help contribute wherever I can!

Most helpful comment

I think if you want props that your styles should react, I would do like this:

const container = p => css({
  py: [3, 4],
  position: p.fixed ? : 'fixed' : 'relative'
});

and like this (styled component creation variant):

const Section = styled(Box)(p => css({
  position: p.fixed ? 'fixed' : 'relative',
  py: 70,
  bg: 'primary'
}))

Generally, I really like the css prop. It is like 'old good' class on element, but with full power of css-in-js and with styled-system css function it also opens convenient access to theme values. With it the era of styled propocalypse on components created with styled system will end ๐Ÿ˜„

All 14 comments

Thanks! I probably don't have as many pointers as you'd like, but maybe some other people can shed some light cc @peduarte @emplums

As for the different approaches, here are some quick thoughts:

  1. I love the css prop โ€“ it lets you quickly write styles where you need them and easily remove them when you don't
  2. Creating new styled components is good when you have a good abstraction and you want to create an API that's easy for people with less experience with CSS to use them
  3. This is a pretty cool pattern, I think I first saw it in @peduarte's work & themeGet is only really a nice API if you use tagged template literal syntax (I generally recommend using object literal syntax unless your copypasta-ing a lot of legacy CSS)
  4. Inline styles can be good for dynamic values (i.e. JS-driven animations, etc.), but you should probably avoid them for styles that are static โ€“ there's nothing wrong with inline styles, but if you have a CSS-in-JS library already, might as well use it

What are the performance implications of any of these methods of creating a component?

You'd probably want to get some RUM (real user monitoring) data from your app to really understand this โ€“ microbenchmarks won't paint a full or accurate picture

Are there any differences in the generated bundle size between the methods?

No idea, but that could be an interesting blog post to write :)

Why even offer themeGet() as a method of getting theme values if it's way longer and does the same functionality as css() but without responsive arrays?

It predates css, and it was mostly a utility for tagged template literal syntax, and it's a safe getter function that shouldn't throw if a value doesn't exist.

// example of using themeGet
const Box = styled('div')`
  color: ${themeGet('blue.3')};
`

How do you write more complex components in the real-world with respects to performance and generated bundle size?

Great question! Again, I'd say get some RUM metrics and go from there.

I noticed that you mention in the v5 migration guide that we should now be compose-ing all of our style functions together for better performance.

With compose, it returns a single function that is only called once per render. Without using compose, each function you add to a styled component will be called separately. I don't have hard numbers for the implications on performance, but am making the assumption that a single call will be more performant than multiple calls

Fair, fair, fair, fair, and......

fair.

Thanks for the explanations on the history of themeGet and compose.

As for themeGet, I'm only using object syntax at this point, so having to curry (props) every time was totally annoying. Looking at it in the context of your string literal is pretty cool though, and not that much more code than writing it within css(). On the other hand, I could see Prettier totally intending the css() function onto the next line and indenting, making all my styles twice as indented as I want. Le sigh.

As for compose, that's fair. Totally makes sense. One function call is faster than two. ๐Ÿ˜„

Personally, I'm looking at my 3rd approach as being the one I end up taking long-term. I do have the additional indention, but that's not that big of a deal. And it keeps my render function tidy by not having to put everything in there. Yikes!

I suppose my question is a bit more related to how styled-components and emotion work under the hood rather than how styled-system works under the hood. I'll try researching in that domain a bit more, as well as getting some RUM metrics spun up. I'll report back if I find anything interesting. Thanks as always @jxnblk!

@cereallarceny Just dropping by to say that I recently wrapped up a bunch of refactoring from raw styled-components into components built with styled-system. I experimented with a few different approaches as well and have settled on your third approach as being the most readable, consistent, and convenient as well.

The only thing I'm struggling with now is when to cut over from using props in a component, like so:

<Box px={[3, 4]} margin="0 auto" {...otherProps} />

...to using the css() approach:

const CenteredBox = styled(Box)(
  css({
    px: [3, 4],
    margin: "0 auto",
    // ...all other props
  })
);

...on a component by component basis. I don't want to go overboard and never use inline style props, but it's hard finding a logical point at which to cut over to css() for a particular case.

Would love to hear your guys' thoughts on that. (I know I'm slightly hijacking the thread since this isn't as directly related to performance, sorry ๐Ÿ™)

Edit: also, props to @peduarte for surfacing this pattern. :)

No, very interesting interjection. No apology needed. I hadn't considered extra props actually. Although I'm sure you can just use the spread operator over the props within the styled component definition. For instance:

const MyComponent = styled(Box)(props => (
css({
px: [3, 4],
...props
})
));

I think that even for one-off styling it's always better to define separate objects with styles, using css function and then push it to css prop (cause I really don't like to see a bunch of styling props in my components):

import css from '@styled-system/css'
import { Box, Container } from '@components/ui'

// some big component
export default () => (
  <Container css={container}>
    a lot of other components
  <Container/>
)

const container = css({
  py: 5,
  position: 'relative'
})

And when you want create new component (from element or extending from existing) the 3d way seems to be the best:

const Section = styled(Box)(css({
  position: 'relative',
  py: 70,
  bg: 'primary'
}))

Interesting @dapetrov - how would you handle use submitted props? Maybe something like this? (I haven't tested if the below actually works.)

// coolbox.js
import css from '@styled-system/css'
import { Box } from '@components/ui'

const container = css({
  py: [3, 4],
  position: 'relative'
});

// some big component
export default props => (
  <Box css={container} {...props}>
    Cool box goes here
  <Box/>
);
// other-file.js
import CoolBox from './coolbox';

export default props => <CoolBox py={5} />

I think if you want props that your styles should react, I would do like this:

const container = p => css({
  py: [3, 4],
  position: p.fixed ? : 'fixed' : 'relative'
});

and like this (styled component creation variant):

const Section = styled(Box)(p => css({
  position: p.fixed ? 'fixed' : 'relative',
  py: 70,
  bg: 'primary'
}))

Generally, I really like the css prop. It is like 'old good' class on element, but with full power of css-in-js and with styled-system css function it also opens convenient access to theme values. With it the era of styled propocalypse on components created with styled system will end ๐Ÿ˜„

@dapetrov How do you handle the user passing props from outside? See my CoolBox example where the user passes in a custom py prop. Likewise, how would you handle passing in props that aren't supported in styled-system or CSS (i.e. if the CoolBox passes user={user})?

I don't want to go overboard and never use inline style props, but it's hard finding a logical point at which to cut over to css() for a particular case.

I would use css() if you don't intend composed elements to override the styles you've declared and use style props (<Box bg='red' />) if you want that behavior. Given your example:

const CenteredBox = props => <Box px={[3, 4]} margin='0 auto' {...props} />
const CenteredBox2 = styled(Box)(
  css({
    px: [3, 4],
    margin: '0 auto'
  })
)

// ...render

<CenteredBox px={5}>CenteredBox</CenteredBox>
<CenteredBox2 px={5}>CenteredBox2</CenteredBox2>

CenteredBox would get px={5} while CenteredBox2 would not. Other than that, I assume it's your call and probably best to discuss with your team and stick with one pattern.

Looking at the @styled-system/css, I think it's meant to be used as a standalone package and not use alongside with style props. If you already have style props, it probably makes sense the most to use css() in the third pattern (as @cereallarceny originally posted) to create abstracted components and get away withprops => props.theme.xyz pattern, so instead of this:

const px = theme => ({
  paddingRight: theme.space[3],
  paddingLeft: theme.space[3],

  '@media screen and (min-width: 40em)': {
    paddingRight: theme.space[4],
    paddingLeft: theme.space[4]
  }
})

const smallpx = theme => ({
  paddingRight: theme.space[1],
  paddingLeft: theme.space[1],

  '@media screen and (min-width: 40em)': {
    paddingRight: theme.space[2],
    paddingLeft: theme.space[2]
  }
})

const CenteredBox = styled(Box)(({ small, theme }) => {
  if (small) {
    return {
      ...smallpx(theme),
      margin: '0 auto'
    }
  }
  return {
    ...px(theme),
    margin: '0 auto'
  }
})

You could do this:

// same as above, but simpler
const CenteredBox = styled(Box)(({ small }) =>
  css({
    px: small ? [1, 2] : [3, 4],
    margin: '0 auto'
  })
)

I haven't tested this @calspre, but would you be able to do something the following:

// main.js
const SpecialBox = styled(Box)(
  css({
    px: [4, 5]
  })
),
color,
space);
// other-file.js
<SpecialBox px={3} fontSize="main" />

I would imagine that in this case, px would be set to 3 and that fontSize would be ignored. Would we even need to pass the space styled-system function at all?

@cereallarceny Does Box have color and space functions? In that case, you remove them from SpecialBox and px={3} would not work. As far as I know, it's the order of styles declared that affects specificity.

Would we even need to pass the space styled-system function at all?

That's why I said in my previous comment @styled-system/css can be used as a standalone package and not use styled-system at all. If you go that route, you don't use style props (e.g. px={3}) and instead use css prop (e.g. css={css({ px: 3 })})

@calspre I haven't tested it but I don't think your CenteredBox example above would work. css() returns a style function right? So for your code to work it would have to be immediately invoked:

const CenteredBox = styled(Box)(({ small, theme }) =>
  css({
    px: small ? [1, 2] : [3, 4],
    margin: '0 auto'
  })({ theme })
)

Going to close this out since it seems like the discussion has mostly died down here

RIP

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wcastand picture wcastand  ยท  4Comments

wmarkowitz picture wmarkowitz  ยท  4Comments

benjamingeorge picture benjamingeorge  ยท  3Comments

ghost picture ghost  ยท  3Comments

eMontielG picture eMontielG  ยท  4Comments