emotion-js / emotion

πŸ‘©β€πŸŽ€ CSS-in-JS library designed for high performance style composition
https://emotion.sh/
MIT License
17.49k stars 1.11k forks source link

Emotion 10 #637

Closed emmatown closed 5 years ago

emmatown commented 6 years ago

Emotion 10 is going to introduce some new APIs for React users.

The main aim with these new APIs is to move all insertion to inside the React tree, this enables a couple of things:

The main new APIs are a custom createElement for the css prop and a Global component for global styles.

Example

The exact APIs are likely to change before Emotion 10 is released.

/** @jsx jsx */
import { jsx, Global } from '@emotion/core'
import css from '@emotion/css'
import { render } from 'react-dom'

render(
  <div>
    <h1
      css={css`
        font-size: 500px;
      `}
    >
      This is really big
    </h1>
    <Global
      css={{
        body: {
          backgroundColor: 'hotpink'
        }
      }}
    />
  </div>,
  document.getElementById('root')
)

If you want to try this, look at https://github.com/emotion-js/next but note that there will be breaking changes very often for now.

Another change that emotion@10 will bring is that styled.div and etc. will work without a babel plugin. The tag list will be included in @emotion/styled but if you use @emotion/styled.macro with babel-plugin-macros or the babel plugin, styled.div will be replaced with styled('div') and @emotion/styled-base will be imported instead which doesn't include the tag list.

Compatibility with Preact and non-React-like libraries

While the majority of Emotion users use React, there are some users who don't use React and we have to figure out how to cater for them.

Non-React-like libraries

Since all the packages on the @emotion scope are very modular, we can build the current emotion APIs with the packages on the @emotion scope. This should be pretty simple.

Preact

There are only a few APIs that we use that are new.

Fragment

Fragments are only used for SSR so we could have an alternative SSR API for that similar to extractCritical now.

createContext

We could tell people to use create-react-context and essentially polyfill createContext.

forwardRef

We could conditionally use forwardRef so if it's not available then we don't use it. I'm not a huge fan of this because I think it would be confusing to tell people they should use ref when using React and innerRef when using Preact but I'm not totally sure yet.

The other option to all of this is to not support the new APIs on Preact and have Preact users use the current emotion API.

Migration

This is something I'm still thinking about and I've got some ideas about but nothing concrete yet. I'm thinking of migrating emotion.sh and seeing what's difficult, how to make incremental migration easy, what can be done automatically with codemods and what warnings would be useful to provide.

Removing extractStatic

extractStatic hasn't been something that we've encouraged for a long time so @tkh44 and I think it's time to remove it. For people who like static extraction, libraries like Linaria and css-literal-loader do static extraction much better than Emotion does so those people should use those libraries.

Only do transformations when emotion is imported

babel-plugin-emotion currently transforms anything with the names of emotion's exports. While this works it transforms things for other libraries (#626 and #344) so we should only transform . I want to do this in Emotion 10 since it could be a breaking change for some people.

TODO

tkh44 commented 6 years ago

This looks great. I like this API quite a bit. Are there any downsides that we should address?

emmatown commented 6 years ago

The main downside is that it'll be hard to get class names outside of render if you're using the new APIs, that's intentional though. Another smaller thing that I haven't really figured out yet is keyframes, currently in the new API, you use it like this

/** @jsx jsx */
import jsx from '@emotion/jsx'
import keyframes from '@emotion/keyframes'
import css from '@emotion/css'

const animation = keyframes(css`
  from {
    color: green;
  }
  to {
    color: hotpink;
  }
`)

const Comp = () => (
  <div
    css={[
      {
        animation: `1s ${animation.name}`
      },
      animation.styles
    ]}
  />
)

which is pretty inconvenient since you have to pass in the name and the styles, for string styles we could interpolate the animation object and insert the animation but for object styles we can't do that since we only have a string when we interpolate the object. Any thoughts on how we could solve this?

Andarist commented 6 years ago

Another change that emotion@10 will bring is that styled.div and etc. will work without a babel plugin. The tag list will be included in @emotion/styled but if you use @emotion/styled.macro, styled.div will be replaced with styled('div') and @emotion/styled-base will be imported instead which doesn't include the tag list.

What is the argument behind this? IMHO it brings unnecessary config right from the start when one wants to use emotion in production (assuming everyone should do it as it's less bytes without the tag list).

We could conditionally use forwardRef so if it's not available then we don't use it. I'm not a huge fan of this because I think it would be confusing to tell people they should use ref when using React and innerRef when using Preact but I'm not totally sure yet.

IMHO consistency is better, so I'd vote for keeping innerRef. In the meantime it might be good to research what preact's plans are about those new React APIs.

extractStatic hasn't been something that we've encouraged for a long time so @tkh44 and I think it's time to remove it. For people who like static extraction, libraries like Linaria and css-literal-loader do static extraction much better than Emotion does so those people should use those libraries.

Even if the feature is not actively developed and in non-ideal state, I don't quite see why should it be removed. It surely has some audience, brings some benefits to the users and having it might encourage outside contributors to make it cover more cases.

emmatown commented 6 years ago

Another change that emotion@10 will bring is that styled.div and etc. will work without a babel plugin. The tag list will be included in but if you use .macro, styled.div will be replaced with styled('div') and will be imported instead which doesn't include the tag list.

What is the argument behind this? IMHO it brings unnecessary config right from the start when one wants to use emotion in production (assuming everyone should do it as it's less bytes without the tag list).

I'm not sure if you're referring to using a babel macro or including the tag list by default so I'll talk about both

It requires some runtime code for styled so removing it would mean styled would be slightly smaller. (I know the amount of code it uses isn't very big, it still helps though) While extractStatic may have some small audience, other libraries do a better job of it so there's no reason people should use emotion over those other libraries. Also, explaining it in the docs is hard because we have to essentially say "we have this thing, it doesn't work very well, we don't recommend it and we're not planning on improving it but it's still here".

If we did keep it and contributors improved it, it would still likely require you to follow certain rules to make it statically analyzable which would limit the power of css-in-js.

Andarist commented 6 years ago

If we did keep it and contributors improved it, it would still likely require you to follow certain rules to make it statically analyzable which would limit the power of css-in-js.

That is true, emotion's (and other css-in-js) true power lies in dynamic per props & theme styling, but at the same time not everything is dynamic in styles and that static part can be extracted. Keeping it would allow perfecting algorithm some day and would combine benefits of static extraction & dynamic styling at the same time. Just my 2 cents though πŸ˜‰

ntkoso commented 6 years ago

Never liked injectGlobal api in any css-in-js library. It's a great idea to move away from globals. Especially with React Suspense and easy streaming on server side globals will be the most annoying thing ever. As for babel-plugin, i was avoiding trying next version before you added it. Thank you. With glamorous deprecation the main 2 css-in-js libraries for me are Styled Components and Emotion :+1:

greggb commented 6 years ago

Really excited about the direction of this. Had a couple of questions while thinking about what I might need to update.

If the css prop will be the favored approach do you envision most components having a bunch of outer scope variables for these styles? I really enjoy the css prop pattern, but it messes with the readability of a complexly styled components. This will somewhat involve class naming for the variables. e.g.

const borderedBox = css`<styles>`;

const Box = props => <div css={borderedBox} ...<lots of props>><a bunch of nested components></div>

Another change that emotion@10 will bring is that styled.div and etc. will work without a babel plugin. The tag list will be included in @emotion/styled but if you use @emotion/styled.macro, styled.div will be replaced with styled('div') and @emotion/styled-base will be imported instead which doesn't include the tag list.

Does this means for optimal perf we'll need to use styled('div') or

if(isProd) {
import(@emotion/styled.macro)
} else {
import(@emotion/styled)
}
styled.div

The css prop works without the babel plugin because it's implemented as a custom createElement

Does this mean doubling the number of dom elements or is it just a React implementation of the element?

emmatown commented 6 years ago

If the css prop will be the favored approach do you envision most components having a bunch of outer scope variables for these styles? I really enjoy the css prop pattern, but it messes with the readability of a complexly styled components. This will somewhat involve class naming for the variables. e.g.

Could you give an example of what you would write now for that case?

Does this means for optimal perf we'll need to use styled('div') or

It only means you'll always be able to use styled.div. If you use @emotion/styled without the babel plugin, you'll include the tag list. If you use @emotion/styled.macro which requires babel-plugin-macros which will be included in create-react-app 2 or you use the babel plugin, styled.div will be converted to styled('div') and @emotion/styled-base will be imported which doesn't include the tag list.

Does this mean doubling the number of dom elements or is it just a React implementation of the element?

I'm not sure if I totally understand what you mean but the only time when there will be more dom elements is when doing SSR and style elements get rendered. You can look at how it works here

greggb commented 6 years ago

Here's a meaty one:

const primaryStyle = css`
    background: ${colorMap.GreenDark};
    border: 1px solid ${colorMap.GreenDark};
    color: #fff;
    font-size: ${size.default};
    &:hover {
        background: #507b32;
        border-color: ${colorMap.GreenDark};
        color: #fff;
    }
    &:active {
        ${primaryActiveStyle};
    }
    &[disabled],
    &[disabled]:hover {
        background-color: ${colorMap.slateNeutral};
        border: 1px solid #e9ebec;
        box-shadow: -2px 4px 2px -4px ${colorMap.slateLight};
        color: ${colorMap.slateLight};
    }
`;

It only means you'll always be able to use styled.div.

Ok, so nothing changes if we're using the babel plugin already. πŸ‘ My concern was that it would be more performant in dev to use the whitelist, but fewer bytes over the wire with the macro (hence a different import depending on env.)

I'm not sure if I totally understand what you mean but the only time when there will be more dom elements is when doing SSR Add a dev warning when :first-child and selectors like it are used and recommend replacements because of problems when rendering style elements in SSR and those selectors targeting the style elements.

Cool, then this won't affect my project atm, but my concern was adding another dom node for every css prop used on the page. For large pages I'd imagine this could add overhead.

Losing :first-child is going to be a little confusing for some of my junior devs when we start server-rending. I wonder if the babel plugin could change :first-child to :first-of-type or first-child:not(<customElement class>)?

emmatown commented 6 years ago

Here's a meaty one:

Sorry, I meant one that would be different with the css prop in regards to this comment. As in, how would the readability change from how it is now and how do you have to name more than in the past?

If the css prop will be the favored approach do you envision most components having a bunch of outer scope variables for these styles? I really enjoy the css prop pattern, but it messes with the readability of a complexly styled components. This will somewhat involve class naming for the variables. e.g.

Losing :first-child is going to be a little confusing for some of my junior devs when we start server-rending. I wonder if the babel plugin could change :first-child to :first-of-type or first-child:not()?

The reason that it isn't done automatically is that if we did it in the babel plugin there would be cases where it wouldn't work and if we did it at runtime it would mean checking every single selector which would be expensive. Also, there's no direct replacement for :first-child that ignores style elements.

greggb commented 6 years ago

Most of the examples and docs so far use inline styles for the css prop. It seems like the goal is to move toward that and away from styled. If that's not true than nothing is really different, I was thinking of new users who would be writing long strings of styles within a component and not knowing there are alternatives.

I thought this might work $('div :first-child:not(style)'), but realized you can't actually select style πŸ€¦β€β™‚οΈ

andywer commented 6 years ago

Hey guys! Quick question: Wouldnβ€˜t it be cleaner to have a customized react-dom with a reconciler that handles the css prop natively instead of patching createElement?

Just a thought πŸ˜‰ Wrote down some ideas about it here when Twitter folks mentioned this issue.

Btw, great stuff, though 😊

Ailrun commented 6 years ago

@andywer Thank you for your opinion! However, as far as I think, it will make emotion too strongly depending on reconciling mechanism of react. The element structure of react is not that big stuff, but reconciling mechanism, especially after React@16, is quite big and complex one...

emmatown commented 6 years ago

@andywer Just to be clear, we're not patching createElement, we're providing a function that you explicitly use instead of createElement.

IMO, creating a custom reconciler would be so much more complex, require a lot of work(and maintenance over time since it would have to be changed every time that react-dom was changed) and wouldn't really have any significant advantages over a custom createElement. Also, what do you mean by "cleaner" and why is having a custom reconciler "cleaner"?

Also, having a custom reconciler would make it harder to share components since you would have to make people use the custom reconciler as well rather than just exporting a component and using it with react-dom. This is especially important because one of the goals with this was to make it possible to create components with styles that can be imported from npm and just work without any configuration including with server-side rendering.

hedgepigdaniel commented 6 years ago

Add an alternative SSR API to get around the caveats with rendering style elements

Can you point me in the direction of what you mean by this?

I've done some experimenting with https://github.com/emotion-js/next and its worked an absolute treat. Feels really clean/simple, and as you said, SSR just works since the style tags come out as part of the standard rendered react component tree.

One good argument in favour of using things like the extractStatic option or Linaria (i.e. extracting style information into css files) is that it avoids the needs to send style information twice on the initial render (Once before/in the server rendered HTML to ensure that the page renders correctly the first time, and once again as part of the javascript so that it can continue to keep the styles up to date after React mounts). With https://github.com/faceyspacey/extract-css-chunks-webpack-plugin and associated packages you can avoid that duplication as long as you put the styles into CSS files somewhere in your build pipeline.

Is that something that is or could be addressed with this new approach of rendering style tags within react? This has got me thinking though - already with SSR you duplicate the HTML (the SSR HTML version and the javascript/React version). I guess this is just an extension of that.

emmatown commented 6 years ago

@hedgepigdaniel The caveat I'm referring to is that :first-child and pseudo classes like it are unsafe because the style elements are rendered next to other elements so the pseudo classes could target the style elements instead of the element they're intended to be targeting.

The new approach doesn't change anything with regards to static extraction. I'm not very interested in static extraction right now, I personally find the trade offs not worth the benefits. I think that the only practical way to do static extraction without big trade offs is with something like prepack.

hedgepigdaniel commented 6 years ago

Ah cool. Since the box is checked... where is the alternative SSR API?

If I do ReactDom.renderToString(<App />) with the latest @emotion/core, the style tags are throughout the DOM tree in the rendered HTML, not in <head />. It sounds like this new approach is a way of SSRing the app such that the styles are all in the <head />.

emmatown commented 6 years ago

@hedgepigdaniel the alternative api is using the current emotion ssr apis with the compat cache.

You can see an example of it in the tests, https://github.com/emotion-js/emotion/blob/master/next-packages/compat-cache/__tests__/server.js

FezVrasta commented 6 years ago

How's going to work the custom createElement with CRA?

Do they support the /** @jsx jsx */ comment and we are going to use it on every single file, or there is some way to change that setting globally?

Andarist commented 6 years ago

I highly doubt they disallow jsx pragma, so it should "just work" with the mentioned /** @jsx jsx */ comment. If you want to change it globally you'd have to use smth like creat-react-app-rewired.

Ailrun commented 6 years ago

Is there a way to disable pragma with babel-plugin-transform-react-jsx? As far as I know, there is no such way...

lifeiscontent commented 6 years ago

anyone know if innerRef has been converted to ref in this branch? It's the only thing that is stopping me from using emotion and the new ThemeProvider is awesome! Looks like I'd be able to hook directly into it, and override some functionality that I need

emmatown commented 6 years ago

Yes, using ref is supported in v10.

lifeiscontent commented 6 years ago

@mitchellhamilton any idea on when v10 might be released?

emmatown commented 6 years ago

It’s in beta, you can see the docs at https://next.emotion.sh

lifeiscontent commented 6 years ago

@mitchellhamilton can I install the beta with yarn add emotion@next?

emmatown commented 6 years ago

Read the instructions at https://next.emotion.sh/docs/migrating-to-emotion-10, all packages not on the emotion npm scope + the react native packages are available on the next tag on npm, all the new packages are on the latest tag.

lifeiscontent commented 6 years ago

@mitchellhamilton I'm looking for an API that works similar to this.

base/Button.js

const Button = ({
  as,
  pressed,
  className,
  disabledClassName,
  pressedClassName,
  ...props
}) =>
  React.createElement(as, {
    className: cx(className, {
      [disabledClassName]: props.disabled,
      [pressedClassName]: pressed,
    }),
    'aria-pressed': pressed,
    'aria-disabled': props.disabled,
    ...props,
  });

Button.defaultProps = {
  as: 'button',
  className: 'Button',
  disabledClassName: 'Button--is-disabled',
  pressedClassName: 'Button--is-pressed',
};

Button.propTypes = {
  as: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
  className: PropTypes.string,
  disabled: PropTypes.bool,
  disabledClassName: PropTypes.string,
  pressed: PropTypes.bool,
  pressedClassName: PropTypes.string,
};

export { Buton };

theme/Button.js

import { Button as BaseButton } from "../base/Button";

const Button = styled(Button)(props => ({className: css(...), pressedClassName: css(...)}));

export { Button };

do you have a recommended way on how to achieve something like this in emotion 10? ideally also having the ability to include variant button styles.

ConAntonakos commented 6 years ago

@mitchellhamilton Awesome changes! Really looking forward to v10. Is it still intended to include a <ScopeProvider/> to help with CSS prioritization and interoperability with other styling techniques/libraries? I see that there is a module called @emotion/provider from the docs. Thank you!

lifeiscontent commented 6 years ago

@mitchellhamilton I found <ClassNames> but it looks like its not properly passing the theme that is injected from the ThemeProvider.

emmatown commented 6 years ago

@mitchellhamilton Awesome changes! Really looking forward to v10. Is it still intended to include a to help with CSS prioritization and interoperability with other styling techniques/libraries? I see that there is a module called @emotion/provider from the docs. Thank you!

We're not planning on including something like ScopeProvider natively but it's possible to build it based on the CodeSandbox I showed. Where did you see @emotion/provider in the docs? It shouldn't be used.

@mitchellhamilton I found but it looks like its not properly passing the theme that is injected from the ThemeProvider.

Are you using the ThemeProvider from emotion-theming@next?

ConAntonakos commented 6 years ago

We're not planning on including something like ScopeProvider natively but it's possible to build it based on the CodeSandbox I showed. Where did you see @emotion/provider in the docs? It shouldn't be used.

@mitchellhamilton Ok, got it. Thanks for the clarification. In order to work with other styling libraries or deal with CSS prioritization, are the traditional methods the best route forward? For example, wrapping an already externally styled component like so styled(ComponentWithJSS) or do you recommend something like the <ScopeProvider />?

Looks like you caught the import mistake: https://github.com/emotion-js/emotion/commit/56581cbb56c4e3386211c740719dfe40095141b9#diff-12703cf35278a283eae498a16e376670. Thanks for updating.

emmatown commented 6 years ago

In order to work with other styling libraries or deal with CSS prioritization, are the traditional methods the best route forward? For example, wrapping an already externally styled component like so styled(ComponentWithJSS) or do you recommend something like the ?

if styled(ComponentWithJSS) works, keep using it.

lifeiscontent commented 6 years ago

@mitchellhamilton good point, I'll check on that and report back πŸ‘

lifeiscontent commented 6 years ago

@mitchellhamilton how would you test a component with a theme using the ClassNames component?

lifeiscontent commented 6 years ago

just wrap each component with a provider? or can you some how specify a default theme?

emmatown commented 6 years ago

Yes, wrap it in a ThemeProvider.

jgoux commented 6 years ago

Hi @mitchellhamilton , I'm giving emotion 10 a try today and I have a few questions :

// I already try that, it returns an object where the name key seems to be the className? const activeClassName = csscolor: blue;



- What's the exact purpose of the exported `css` from `@emotion/core`? Is there any access to the props when using `css` ?

So far it seems promising! ⭐️ 
emmatown commented 6 years ago

Is it planned to add automatic insertion of /* @jsx jsx / with babel-plugin-emotion? (I saw the mention Alternatively, use this TODO babel preset in the migration guide)

You can use eslint-plugin-emotion which will add it.

Is ClassNames necessary for passing a className to a component?

In general, you should use the css prop, ClassNames should rarely if ever be used.

If we use babel-plugin-emotion, is it possible to use this syntax instead

No.

What's the exact purpose of the exported css from @emotion/core?

Syntax highlighting, performance, allowing interpolations, you pass the result of the css to the css prop.

Is there any access to the props when using css?

No.

jgoux commented 6 years ago

You can use eslint-plugin-emotion which will add it.

I was more looking for a compile step than a codemod. Ideally I'd like the pragma to be absent from my code. For example this plugin (https://github.com/satya164/babel-plugin-css-prop) looks for a css prop in JSX declaration so I think it would be possible to rewrite the pragma if the css prop is used?

In general, you should use the css prop, ClassNames should rarely if ever be used.

My use case is the activeClassName prop from react-router Link component. I don't really understand why a render-prop is necessary to get the className with css.

Thanks for your answers! πŸ‘

EDIT : It seems that there is already a plugin for injecting the source of the pragma ! https://github.com/WordPress/gutenberg/tree/master/packages/babel-plugin-import-jsx-pragma I'm not sure about the performance if we use it for every JSX declaration instead of checking for the presence of the css prop first.

EDIT II : create-react-app makes it difficult to add a new pragma globally. There is an issue about it : https://github.com/facebook/create-react-app/issues/5152

ncknuna commented 6 years ago

I have a similar need to regularly pass a className to a component from a third party library (in my case, Ant Design, https://ant.design/, which many of my UI components wrap and add additional styles to). I can make the ClassNames render-prop work, but having a lighter-weight solution would be nice...

emmatown commented 6 years ago

My use case is the activeClassName prop from react-router Link component. I don't really understand why a render-prop is necessary to get the className with css.

It's necessary to allow the out of the box server rendering and to access context so things like iframes and providing options can work via context

With adding the pragma globally, I don't really understand why people find it so important? It takes two lines to import and set the pragma and for that, it makes it easier for people who may not have used emotion or the css prop to understand why the css prop gets converted into a className since they can see that the jsx pragma has changed, look at emotion from that and etc.

jgoux commented 6 years ago

It's necessary to allow the out of the box server rendering and to access context so things like iframes and providing options can work via context

Ok, I can live with it. Thanks again for explaining me the tradeoffs. πŸ‘

With adding the pragma globally, I don't really understand why people find it so important? It takes two lines to import and set the pragma

As a developer I'm lazy and if I can automate a thing and type less then I always go that road. We should at least have the possibility to do it even if it's not officially advertised.

it makes it easier for people who may not have used emotion or the css prop to understand why the css prop gets converted into a className since they can see that the jsx pragma has changed, look at emotion from that and etc.

With the current ecosystem and the heavy usage of compilers such as Babel, I find it more understandable to rely on a plugin to transform my code globally (when the plugins usage are well explained). It also reduce the technical noise in my codebases, every line not written is a win, especially when these lines are about implementation details and can be automated. If https://github.com/WordPress/gutenberg/tree/master/packages/babel-plugin-import-jsx-pragma exists for something as huge as wordpress then maybe there is a use case for it.

This is all my own opinion of course! πŸ˜‡

emmatown commented 6 years ago

We should at least have the possibility to do it even if it's not officially advertised.

Yep, I agree, and it's totally possible, you can use the babel plugin you mentioned, the emotion site even uses a similar plugin. We may even create a babel preset to do that but the one thing that I'm afraid of is that I don't want to create too many choices for people when they want to use emotion so if we add babel preset it likely won't be the primary way we intend for people to use it since importing it and setting the jsx pragma works everywhere whereas adding a preset only works when you have access to the babel config.

jgoux commented 6 years ago

For now I gave a possible userland solution here for anyone interested.

Thanks for the answer @mitchellhamilton , and amazing job on emotion 10! πŸ‘

One last question, does a styled component requires the custom pragma?

For example with the future as property, if I define a Box primitive like this :

import styled from '@emotion/styled'
import { AppBar } from '@material-ui/core'

const Box = styled.div``

const App = () => (
  <Box as="main" css={css`width: 100vw; height: 100vh;`}>
    <Box as={AppBar} css={css`font-size: 4em;`}></Box>
  </Box>
)

does it requires the pragma transformation or is the css prop already built-in into the styled component?

Andarist commented 6 years ago

@jgoux https://github.com/emotion-js/emotion/pull/917

Emiliano-Bucci commented 6 years ago

Hi to all, i'm encountering some issues using the new version of Emotion with next.js and typescript. The fact is that every time i use /** @jsx jsx */ import { jsx } from "@emotion/core"; i always have the same error: ReferenceError: jsx is not defined I've tried to made a fresh install (without typescript) and works fine, so maybe is an issue with typescript? Or a babel next.js config? Does anyone had the same problem? Thanks a lot :)

Jessidhia commented 6 years ago

https://github.com/babel/babel/issues/8958

Emiliano-Bucci commented 6 years ago

@Kovensky Sorry but, what do you mean by

Adding an unused jsx reference anywhere to the file makes the build correct

Jessidhia commented 6 years ago

You can literally just write a random jsx; statement anywhere in the file. Linters hate it!

But it works...