Emotion: .attrs equivalent

Created on 24 Aug 2018  路  24Comments  路  Source: emotion-js/emotion

This was mentioned in #109 just over a year ago but with very little discussion around it so I hope you don't mind me raising it again.

I've been toying with the idea of switching a very large project over from styled-components to emotion for a while now. Today I bit the bullet and tried doing a mass conversion of thousands of components.

I was very pleased and surprised with how straightforward this was to do thanks to your similar APIs. However, the one feature I'm sorely missing is the ability to define default props; .attrs.

Again, in #109 this was raised but it looks like the suggested approach is to use recompose and withProps. Personally I think this would be better as part of the library.

Much of my conversion was a simple find and replace job, but converting .attrs required a comparatively disproportionate amount of effort. Having to depend on another library for the purpose of importing a single function is fine I suppose, but the syntax is also a bit jarring. The nice thing about the styled API is how readable it can be... however when a styled component with some logical defaults comes along it takes a bit more mental parsing than seems necessary.

Compare

const Input = styled.input`
    border: thin solid red;
`;

const Password = styled(Input).attrs({ type: 'password' })`
    color: blue;
`;

or

const Input = styled.input`
    border: thin solid red;
`;

const Password = styled(Input, { type: 'password' })`
    color: blue;
`;

with

const Input = styled.input`
    border: thin solid red;
`;

const Password = withProps({ type: 'password' })(styled(Input)`
    color: blue;
`);

I know much of this is personal preference, but the withProps variation looks so unlike a 'normal' styled component yet the act of specifying some sensible default props isn't really that exotic, is it?

Anyway, just thought I'd put this out there. I know there is personal preference and library 'purity' as factors too. It just seems to me this would be a tiny addition but one which may help fellow wannabe styled components users like me take the leap.

Thanks.

discussion

Most helpful comment

Ok, so I'm not sure if the .withProps is the official recommendation. Personally I would recommend doing this:

const Column = styled(Flex)`
    color: red;
`;
Column.defaultProps = { direction: 'column' }

All 24 comments

We've discussed this before and we've come to the conclusion that the best way to do this is recommend people use the css prop and attach props just like you would any other react component.(the reasons are described in the linked comment)

Thanks. That's the first thing I tried though there is a gotcha in that the component becomes stateless so can't have refs. This is an issue for libraries which use refs to attach behaviour to elements (react-dnd, react-measure, etc.)

I imagine you could use forwardRef these days but it then starts looking a lot like the withProps code above

@jamiewinder what's the gotcha with ref regarding defaultProps (which .attrs are, right?)? Seems like separate subjects but maybe I have misunderstood smth.

Stateless components can't have refs, so while a standard styled component can be used with things like react-dnd and react-measure without too much trouble (just use innerRef rather than ref), you can't do the same with a stateless wrapper that provides some default props (like the example @mitchellhamilton gave in his reply).

You can if you use forwardRef or just by using withProps (since that creates a class-based / stateful wrapper) but then the code starts becoming (imo) unnecessarily messy like the code above, all for the sake of supplying some logical defaults props that go hand-in-hand with styling when it comes to creating semantic components.

For example, if I have a styled <Flex> component which takes a direction prop, then I might logically want to define a <Column> component which extends Flex, defaults the direction to 'column', and maybe adds some styles of its own.

If I use the approach suggested above, then I suddenly can't use react-measure to measure my Column component. Not without using forwardRef to wrap the component but that's just more noise in what ought to be as readable as a stylesheet.

In styled-components, I can simply:

const Column = styled(Flex).attrs({ direction: 'column' })`
    color: red;
`;

Emotion is already effectively tying props to styles and when components extend other components and may want to define default props (ergo default styles) you'll want to make this as straightforward as possible without the visual noise withProps adds and the pitfalls of a stateless component wrapper (i.r.t. refs as described above).

It just seems to be a logical extension of the emotion's "style as a function of state" tagline - default props = default styles when you're extending one styled component from another.

Ok, so I'm not sure if the .withProps is the official recommendation. Personally I would recommend doing this:

const Column = styled(Flex)`
    color: red;
`;
Column.defaultProps = { direction: 'column' }

Thanks, I think that's my preferred method so far.

Did this ever go anywhere? Seems really useful!

I think the method mentioned by @Andarist just before are such as good as the .attr() way? and also more React familiar.

I'll go with @Andarist's solution. This is the best solution as it makes sense to add type as a default prop. Thanks man, I almost forgot this specific advantage of using defaultProps

The preferred way of doing this is just using @emotion/core APIs and because with them everything is done inside of your component's render there is no need to introduce any custom API to emotion itself, because you can just use JavaScript to handle your stuff. If you like styled API you can also easily build your own flavor of it based on the @emotion/core APIs (taking @emotion/styled-base implementation as a base for it - as it is actually just a wrapper around @emotion/core).

EDIT:// Oh, and I would not recommend using .defaultProps nowadays as React team is planning to remove support for this entirely in the future - they are taking "just JavaScript" philosophy as the argument for that.

I'm not clear on what the recommended alternative is. Can you clarify @Andarist, or is there some link to a relevant doc? This issue was the best info I found on achieving this end. Turns out it was a bad week to implement defaultProps for styled components.

You can just not use styled API at all, but if you really want to you can make hoops like this - https://codesandbox.io/s/emotion-uj5z9

import styled from "@emotion/styled";

const customStyled = (tag, options) => {
  const styledFactory = styled(tag, options);
  return function() {
    const Styled = styledFactory.apply(null, arguments);
    Styled.attrs = extraProps => {
      const WithAttrs = props => {
        return <Styled {...extraProps} {...props} />;
      };
      for (let key in Styled) {
        WithAttrs[key] = Styled[key];
      }
      Object.defineProperty(WithAttrs, "toString", { value: Styled.toString });
      return WithAttrs;
    };
    return Styled;
  };
};

I'm trying to understand... the proposed solution is to implement the feature ourselves?

How does this plays with the Babel Macro version? Can I just import @emotion/styled/macro and it will work?

I'm trying to understand... the proposed solution is to implement the feature ourselves?

We believe that this particular feature is not worth including in the core. We need to think about APIs we provide and can't just satisfy each possible feature request. Also please see this comment - https://github.com/emotion-js/emotion/pull/617#issuecomment-388284739 .

How does this plays with the Babel Macro version? Can I just import @emotion/styled/macro and it will work?

Unfortunately not.

Also, regular React-way of composition works for this just OK.

const Input = styled.input``
const TextInput = props => <Input {...props} type="text" />

Even if this seems like a boilerplate this is just a standard way of composing React component, no special APIs, just React and JavaScript.

I see, to be honest I feel like this is a quite common use case. I will most likely use .defaultProps until React will support it, since it seems the cleaner approach.

But if defaultProps is ever going to be removed I feel like some easy to use API provided by Emotion directly will be much needed.

The React composition method works, but it definitely seems like a big step backwards from the simplicity of the defaultProps. I understand if the emotion team thinks it's not worth the effort required to make/support the feature, but the composition method is more verbose and repetitive.

There are multiple ways to achieve this - without needing to put this into the core. There is always a recompose~ approach (withProps) which you can also implement yourself with 1-liner:

const withAttrs = (Component, attrs) => props => <Component {...attrs} {...props}/>

Thanks @Andarist! While still not as clean as defaultProps, the recompose method seems better than any of the other approaches.

recompose withProps does not send props (attributes) from the HTML element (StyledComponent):

const Range = withProps({ type: 'range' })(styled(Input)`
    color: blue;
`);
<Range min="0" max="1" step="0.01" onChange={handleVolume} value={state.volume} />

Type 'string' is not assignable to type 'never'

One thing that isn't noted here is that attrs() allows to map props.

const MyComponent = styled.div.attrs(props => {
  return {
    cssWidth: props.width
  }
})`
  width: ${props => props.cssWidth};
`

MyComponent.propTypes = {
  width: PropTypes.number
}

Usage without providing a function doesn't necessarily give any value over the defaultProps approach.

Having support for the above use case would massively help in migrating from styled-components to emotion.

Is there any way to do what @Andarist suggested with the object notation?

const Column = styled(Flex, { color: 'red' });

This doesn't seem to work.

@verekia I'm not sure what you are requesting here, but the used syntax is wrong - take a look like how this kind of thing should be written: https://codesandbox.io/s/xenodochial-http-ydqkx

Thank you very much! It works well :)

Maybe it would be useful to add this example to the Styled documentation. There is no example with this syntax: styled(foo)(styles) if I'm not mistaken.

PRs are welcome :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MrFungusEli picture MrFungusEli  路  3Comments

artooras picture artooras  路  3Comments

tkh44 picture tkh44  路  3Comments

sami616 picture sami616  路  3Comments

mitchellhamilton picture mitchellhamilton  路  3Comments