Let me just start by saying, love linaria; Huge fan. A journey of my own for incredibly aggressive tree shaking has lead me to this feature request.
It simple.
Take all of these...
const Btn = styled.button`
color: red;
`;
const Button = () => {
return (
<Btn>I am a button</Btn>
)
}
And instead of outputting
const Btn = styled.button({
class: "asdsadas",
name: "Btn
})require("whatever.css");
const Button = () => {
return (
<Btn>I am a button</Btn>
)
}
Change the output to.....
require("whatever.css");
const Button = () => {
return (
<button className="asdsadas">I am a button</Btn>
)
}
cssHi @ShanonJackson!
It's an interesting proposal, but I'm afraid that we have some unsolvable problems here. For example, what should be exported if we write something like this:
export const Btn = styled.button`
color: red;
`;
Btn can be used for interpolation and composition not only in the same project, but it can be part of a public API of UI library.
@Anber Good point, it could be good if this was a configurable option as to allow faster builds for dev and to allow library authors to expose class selectors as exports. As well as to meet the needs of people chasing better performance and smaller bundle sizes.
EDIT: The inspiration for this post actually comes from
https://www.youtube.com/watch?time_continue=2&v=9JZHodNR184&feature=emb_logo
In which facebook literally just rebuilds linaria except with worse syntax (object literals yuck) and the crowd claps. Good ideas on how to transpile the output super small though that was the take-aways i got.
@ShanonJackson First of all, it won't make builds faster because it requires much more complex AST analysis through the whole project than we have now.
Another point that we still have problems here:
β’ what should we do with dynamic styles (based on props)?
β’ what's about other HOCs which can be applied to a styled component?
Sorry just to clarify i meant if its configurable it would be faster for dev because you'd turn it off for dev not turn it on.
Both HOCS and Dynamic styles cannot work if exported as a library but again internally in a non-library mode they both can work.
const Btn = styled.button`color: ${(p) => p.buttoncolor === "red" : "blue" : "yellow")}`
<Btn buttoncolor={"pink"}/>
// to... (and obviously in lining the css variable hash in the style sheet
`<Btn style={{"--hash": "pink" === "red" : "blue" : "yellow }}/>`
// which when it goes through terser or others i think it should become this? could be wrong.
`<Btn style={{"--hash": true : "blue" : "yellow }}/>`
// then this.
`<Btn style={{"--hash": "blue" }/>`
In my mind this can work for all dynamic expressions, if needed just inline the actual function itself aswell.
@Anber Because for this proposal to work and not be slow in dev you'd need two modes
{inline: true/false} then this option can be turned false for library authors to leave exports intact (For both dynamic styles, HOC's and styled tags). This option can be turned true for application maintainers or left false to solve any backwards compatibility issues they think will occur and turned true for those who are willing to increase build times for less javascript output.
EDIT: And should HOCs/Dynamic styles not be inlined for other reasons that are impossible to solve we can detect this and not inline (Worst case scenario)
What if buttoncolor is dynamic and we cannot predict its value on compile time?
How about components like this:
const BareComponent = (β¦) => <>β¦</>;
const StyledComopent = styled(BareComponent)``;
const FinalComponent = connect(StyledComopent); // How it will look after compilation?
I mean that if you don't need complex styled components you can just use css:
const btnClass = css``;
const Button = () => {
return (
<button className={btnClass}>I am a button</Btn>
)
}
It's more transparent and doesn't require extra runtime.
<Button buttoncolor={props.dynamicvalue} prop1={expression} prop2={expression2}/>
// becomes.
<button
styles={{
"--hash": ((p) => p.buttoncolor === "red" ? "blue" : "yellow")({buttoncolor: props.dynamicvalue, prop1: expression, prop2: expression})
}}
/>
In this case the function itself can become inlined executed with its dynamic expressions as arguments
css is indeed a workaround but i find it extremely "smelly" i use linaria to write css-in-js with styled-components like syntax and zero runtime cost. I have no real valid reason why this isn't an acceptable solution right now but i do feel like just spamming css everywhere walks a fine line between "linaria" and "just use css modules"
Inlined functions and objects can cause performance problems due to the inability of shallow comparisons so it will be a bit more tricky. Anyway, I've got your point and have to think about it.
Gracias @Anber let me know when there's any updates.
Though I see the potential issues with this idea, there's actually another good reason to consider it. With its current implementation, styled components take more than twice as long to render as elements using css class names. If it were possible to double performance (at least on components non-dynamic styled components) and use the much nicer styled api, that would be really awesome.
Instead of trying to optimize the styled API, which I feel will have many edge cases, maybe it'll be better to introduce a new API:
function MyComponent({ color }) {
const styles = style`
background-color: ${color};
color: ${isLight(color) ? 'black' : 'white'};
min-height: 200px;
`;
return (
<div {...styles}>
Whatever
</div>
)
}
// β β β β β β β β β β
function MyComponent({ color }) {
const styles = {
className: 'gr6rt4e',
style: {
'--a': color,
'--b': isLight(color) ? 'black' : 'white',
}
}
return (
<div {...styles}>
Whatever
</div>
)
}
/* Personal opinion */
inline styles based on stateful variables in 99.99999% of use cases will not need to be overridden by css in which case specificity doesn't matter; In the remaining 0.1% of use cases use !important to override the inline style, that's what !important is meant to be used for and using under that context is definitely not a code smell.
I don't use the css variable dynamic update API for this reason and instead do all my modifiers BEM style with css + cx and my stateful css just with plain old inline styles style={{width: RuntimeValue}};
I feel like linaria should move further from this css variables API not closer towards it.
/* End personal opinion */
On another note i feel that just because there's going to be many edge cases should not scare us off wanting to implement this; There are definitely migration paths that can allow us to mitigate the edge cases such as only inline under extremely strict conditions at first and then as we solve more and more of the edge cases loosen that strictness.
I.E Initially on first release of inline mode only enable it if the option is true AND the styled tag has no dynamic styles and is not a HOC.
Then do another release with the edge cases for HOC's solved and add them to the inline white-list
Then do another release with the edge cases for dynamic styles solved and add them to the inline white-list.
I'm not saying we should do this nor am i saying it will prevent edge cases i'm just saying there are definitely ways to mitigate the possibility of edge cases breaking builds.
Even if there weren't many edge cases, why not implement a better API anyway? ${color} is much better than writing ${props => props.color}.
css just with plain old inline styles
Inline styles for dynamic styles have a lot of limitations. Specificity aside, they don't support pseduo-selectors, media queries etc. They don't work for even simple use cases like making a button component based on theme colors which lightens its colors on hover.
css is indeed a workaround but i find it extremely "smelly"
If you don't use dynamic styles, css is perfect for your use case. It's simple to understand and use. It's also a first class API in Linaria, not a workaround.
The main idea behind styled API was to create styled components which encapsulate styles and can have dynamic styles. If you don't want either of these, css is a much better API.
I really like the styles βinline stylingβ API proposed by @satya164. I am a UI library author and am trying to steer away from styled for various reasons, but am hitting the limitations described above regarding inline styles and pseudo selectors etc. until now I have been dealing with it by setting an in-line custom css property which I access in css for pseudo selectors, and recommending style for simpler use cases. A styles API would solve this and make it conceptually simpler for downstream users
I don't see @satya164's proposal as meeting the criteria of the issue. The idea is to preserve the ease and portability of the styled api, while increasing performance in the places possible. Additionally, the ${(prop) => value} api might seems ugly on the surface, but it actually enables some really cool interfaces. (See styled-is and styled-by, among others).
Linaria already requires prop functions to appear as values in css. This restriction is actually a benefit to optimization, as it makes it much easier to divide styled props that need the current api from those that can be optimized.
This component needs to leverage the full styled api because one or more properties depend on a function:
const Dynamic = styled.div`
top: 0; // static
left: ${leftVal}; // static
bottom: ${(props) => bottomVal}; // dynamic
`;
This component doesn't use prop funcs, so it can be optimized:
const Static = styled.div`
top: 0; // static
left: ${leftVal}; // static
`;
Nothing prevents Linaria from flattening Static with a plain HTML element + class, as @ShanonJackson asserted up top.
Thinking about implementation, what if, instead of doing even more AST processing, Linaria monkey-patched React.createElement, so that any styled component tagged as flattenable was transformed there?
Additionally, the ${(prop) => value} api might seems ugly on the surface, but it actually enables some really cool interfaces. (See styled-is and styled-by, among others).
The styled-is API is never going to be usable in Linaria. Linaria's dynamic styling is limited to property values as that's what CSS variables support.
Both of those APIs exist because styled API needs to use a callback to access props and it can be simplified using those APIs. But what's the use case for those APIs to exist if you could use the props directly anyway?
The styled API also has several disadvantages, in addition to having to define several components for what could be done with only one, there's is also a need to filter out invalid props manually because the API encourages passing extra props to customize the styling which will produce warning from React if passed down to the underlying DOM element. This not only makes the bundle slightly bigger, but the filter needs to run every render. It's probably ok, but if this doesn't matter, one extra wrapper component shouldn't matter either. Except very perf sensitive cases, where both would matter.
The styled API is popular, but it's not the best API, as you can see by the css prop API that a lot of people prefer.
The idea behind an alternative API is to overcome all the limitations of the styled API and benefit all components by not needing an extra wrapper for anything, not just the ones without dynamic styles.
The styled-is API is never going to be usable in Linaria. Linaria's dynamic styling is limited to property values as that's what CSS variables support.
Sure, we're limited with _how much_ of styled-is and related libraries we can use. But that still leaves a substantial portion. I have a custom implementation in my app of is and by that better matches Linaria.
The styled API is popular, but it's not the best API, as you can see by the
cssprop API that a lot of people prefer.
I agree with you on this point. It's not the ultimate solution. However, we don't know if this problem has an ultimate solution. If it did, I suspect it would be more along the lines of styled than css.
With css, there's discontinuity between component logic and classes, especially since classes can't handle every use case on their own. Some styles are better defined on the element itself as inline styles. So if any interface had a hope of being optimal, it would be one where style logic was defined together, and the compiler determined which attributes were static, which could be abstracted to subclasses, and which should be inline.
You can easily imagine a styled-is like interface where multi-property values became subclasses, but single-property values became variables or inline styles. I'm not suggesting we take this particular issue down that rabbit hole, but it seems like a good direction.
The fact is, nobody uses Linaria because they like _using_ it more than than like styled-components. They use Linaria because they like the performance gains it offers. Therein lies the difference between css and styled. If it didn't more-than-double render times, everyone would prefer styled. But if we can keep the benefits of styled while mitigating the performance issues, all the better.
Both of those APIs exist because
styledAPI needs to use a callback to access props and it can be simplified using those APIs. But what's the use case for those APIs to exist if you could use the props directly anyway?
Part of it is shorthand. It's very easy to read and reason about styled components. And when you use them, it actually improves the readability of your components. Nested divs and spans are nothing compared to named elements.
I really have to reiterate, I do understand you about the downsides of styled components. But I don't think the solution should be to throw the baby out with the bathwater. Instead, I think it makes more sense to take what works about the underlying mechanics of Linaria and fuse them with what works about the interface of styled components.
If it didn't more-than-double render times, everyone would prefer styled.
I disagree. It's a little off-topic, but I think the styled API and its abstraction of the DOM encourages poorly composed semantics. I would be very happy to strictly compose static (but scoped) styles with css and dynamic styles with component-level styles. However I also understand that it is not completely relevant to what you're proposing here, so I think that could be moved to a separate discussion.
I have decent experience with babel-AST parsing and i might tackle this one solo when i get time late into February. Unfortunately because of babels inability to understand multi file scope and/or variable usage we're going to need to create webpack/rollup plugins which do give access to multi-file scope and variable usage to transform inline.
Will post results here in terms of how much KB it shaves off my bundle and how much it increases my paint timings hopefully by end of February if nothing else comes up
@satya164 @Jayphen @Anber @mikestopcontinues
In
// Button.styles.ts
export const ButtonBase = styled.button`color: red`;
// Button.tsx
import { ButtonBase } from "./Button.styles";
export const ButtonComponent = () => {
return (
<ButtonBase className={"exitingclass"}>HELLO WORLD</ButtonBase>
)
}
Out
// Button.styles.ts
export const ButtonBase = "asda23as" // import preserved for library authors.
// Button.tsx
// Import stripped now meaning non-library authors WONT have it included as it will automatically // get shaken out however if another file that's included in the bundle exports it it will be included.
export const ButtonComponent = () => {
return (
<button className={"asda23as " + "exitingclass"}>HELLO WORLD</button>
)
}
Note this is PURELY a demo of how it could work,
https://github.com/ShanonJackson/babel-plugin-linaria-inline
npm i && npm run build and look at /dist.
Changed suprisingly little code however evaulate() which handles nested evaluations isn't working it still thinks ToasterBase is an import statement for some reason i dont have time to look into it; Other than that changed very little to achieve this the only reason there's so many files floating around is because alot of linaria's inbuilt stuff references linaria/lib/babel/index so i had to duplicate those files to reference my changed copy.
How it works:
styled.div to css if it detects no nested selectors are detected AND its in production.<ToasterBase> becomes <div>cx(Foo, Bar) becomes {hash + cx(Foo, Bar)}evaluate and others to be able to build my own library but my suspicion is it that it will be exactly the same as linaria styled vs className={string} performance benchmarks floating around already.Because somethings come at work i sadly will not be able to pursue this any more for another 1-2 months so i would hope this is enough for linaria maintainers to look at as a proof of concept and hopefully implement.
Would love to hear feedback.
EDIT: After lots of thinking iv'e realized this is close to the "ideal" but has problems. The actual solution should leave Shared.styles.ts rather than in-lining the result of Shared.styles.ts into another file and removing the imports. This is because otherwise if file "A" imports Shared and file "B" imports Shared you will end up with two style sheets A.css and B.css both with the inlined Shared.stlyes.ts styles.
To fix this i will change the plugin to leave the imports intact; and simply modify the JSX usage with the tag name + className. This will also resolve the evaluate() bugs and should make this plugin ready-for-production soon.
Would still love to hear feedback from the linaria team i believe nothing mentioned here is a valid reason why this shouldn't be built into the library; If you can inline without losing any functionality then its basically a under the hood change for huge performance gains (which as above mentioned is why people use linaria).
After more thinking; Everything can be inlined always regardless of whether its a HOC/Library Author/Css/Styled syntax.
This is how it works:
File A imports File B.styles
File A inlines everything from file B into its markup in this way:
also becomes css where the output references css variable names which are inlined into style={{--varName: FUNCTION}} where FUNCTION is lifted up as high as possible in scope (just walks the AST up).export Button from "./B.styles" meaning it will be included in the output.And now that solves everyone's issues in this thread; I will probably start building this with babel-plugin-macros just for fun but i'd love if linaria implemented this.
@ShanonJackson , I don't know how, but I missed your previous post. This looks pretty darn sweet, and I'll be glad to give it a spin when you've got it up and running.
@mikestopcontinues its not production ready yet; Been working on some other stuff and haven't had time to take another look at it but was hedging my bets on the Linaria team taking the torch to the finish line.
Great idea! The first thing I wanted when I saw the build output was the removal of intermediary React nodes for simple cases like styled.div, styled.button, etc.
This final state as described by @ShanonJackson would be ideal:
const Button = () => {
return (
<button className="asdsadas">I am a button</Btn>
)
}
No styled.button, which in theory should help React stay more out of the way of the program
Most helpful comment
Instead of trying to optimize the
styledAPI, which I feel will have many edge cases, maybe it'll be better to introduce a new API: