-
Notifications
You must be signed in to change notification settings - Fork 616
[Accepted] ADR: Use Box + sx to author components #2020
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
size-limit report 📦
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for writing this, @siddharthkp! 💖
|
||
- Better authoring experience with typescript. With Box, we can improve the API and autocomplete for consuming primitives. | ||
- The styling library used used is an implementation detail and we should be able to replace it. (Avoid leaky abstractions) | ||
- We have had issues with exporting types, we can increase confidence by keeping the exported types close to what we author. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand on the issues we've had with exporting types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry for my lazy phrasing here 😅
What I'm trying to convey here is that
- We've had a couple of bugs like #1959 where we were extracting types from a "styled component"
- Folks on the memex side have reported that the typew are hard to debug because they have a lot of layers (styled components + styled system)
I feel more confident having more control over what we export + keeping it as flat as possible. Types deserves an ADR of it's own, to be honest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be useful to amend the ADR to include some of these additional details for context, either now or in a follow-up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to create a new ADR for types (in new PR), there's more to discuss there because we don't have a standard way right now to propose :)
Co-authored-by: Cole Bemis <colebemis@github.com>
Co-authored-by: Cole Bemis <colebemis@github.com>
thanks Cole! Co-authored-by: Cole Bemis <colebemis@github.com>
As discussed among the PRC maintainers group, let's try assigning a decision-maker here so we can move this ADR toward a conclusion. ADR decision maker: @rezrah I'm going to work on more enduring documentation about this process, but in the meantime: Your role as decision maker is to gather feedback, probe for discussion and disagreement, and then make an informed decision based on the feedback, identified risks, and trade-offs about whether to adopt the ADR or not. Your role is not to decide based on your own personally preferred approach. Keep in mind that ADR decisions can be reversed or changed in the future if new information is exposed that make the ADR worth revisiting. You can fulfill your decision maker role by doing the following:
|
|
||
Prefer using method #2: Creating components with Box for the following reasons: | ||
|
||
- Better authoring experience with Typescript. With Box, we can improve the API and autocomplete for consuming primitives. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@siddharthkp - can you link to your research, which you mentioned in this comment from the ADR itself for easier discovery? I think it will help to elaborate why working with Primitives might be easier for anyone reading it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
const styles = { | ||
display: 'inline-block', | ||
overflow: 'hidden', | ||
lineHeight: 'condensedUltra', | ||
borderRadius: getBorderRadius({size, square}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our goals as customers/consumers are:
- Ease-of-use in terms of authorship (Be it specifying colors, styles, customization, etc)
- Implementing something that is performant with little to no friction
- Strongly typed guardrails, so it's easier to implement ("Do I preface this color with
colors.
? Do I pass a theme here? What does 'space.2' mean?")
I see some pros and cons to this approach:
Pros:
- This allows consumers to more easily define their styles without as much overhead
- It's coupled with the component, so there is less mental friction
- It still provides a solution that is responsive based on props (i.e. dynamic styles)
Cons:
- The biggest drawback I see is that these styles, because they are implemented inside of the component, would be re-computed every single render cycle. While this might be improved with memoization, I still think there's a good deal of perf impact with this approach
- I'm not a huge fan of each component needing to run
merge()
to get styles to work properly, not to mention the added dependency that each app would need to install - I'm concerned about the mental friction of implementing everything as a
Box
and transmuting them into other components by way ofas
. I always thought of that as an escape hatch more than a common pattern.
I definitely think there are improvements to be made on the implementations of styles, specifically styled-components, but I'm also curious to see what styled-components' plans are now that React 18 is released with all of their improvements and DX hooks for these sorts of libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the intention is to require a dependency on merge @dusave but just that it's the consumers job to merge local and props sx if necessary?
if Box
is a Memo
component, could we avoid some overhead by doing a deeper comparison on sx props
memo(function Box(props) {...}, function shouldComponentRender(prevProps, nextProps) {
compare sx deepy, but maintain as object.is equality otherwise
})
which might avoid some/many performance differences from inline definitions of sx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about the mental friction of implementing everything as a Box and transmuting them into other components by way of as. I always thought of that as an escape hatch more than a common pattern.
this, i think would be just something to extract to module scope and use from there, except when needing the props. how much non-primer usage actually changes based on props though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad we're talking about this. the sx implementation could use a lot of improvements!
The goal of the ADR to standardise using Box + sx for creating new components in primer/react (we are already doing that for a few months), so we can prioritise solving the remaining problems with this approach.
The biggest drawback I see is that these styles, because they are implemented inside of the component, would be re-computed every single render cycle. While this might be improved with memoization, I still think there's a good deal of perf impact with this approach
You're not wrong. We need work performance improvements to make sure the page doesn't slow down over time because of re-renders.
However, injecting styles is the biggest task in the whole process. Because Box still uses styled-components to inject styles in the end, the same styles are not injected twice. The exception to this is prop-based styles which need to be re-evaluated and the additional work required by styled-system on top of styled-components could be a bad fit for use cases like javascript-based animations.
I'm not a huge fan of each component needing to run merge() to get styles to work properly, not to mention the added dependency that each app would need to install
Same. I have a few API ideas on how to simplify the API and abstract the merge away from the component.
For the merge dependency, right now it comes from import {merge} from '@primer/react'
, but with API changes we should be able to remove it from the public API.
I'm concerned about the mental friction of implementing everything as a Box and transmuting them into other components by way of as. I always thought of that as an escape hatch more than a common pattern.
Can you expand on the mental friction a bit more.
I think you only need to drop down to Box when you're building a new atomic component. <Box as="img">
is equivalent to styled.img
.
For most use cases, you should be able to start with a primer/react component, especially as we get more layout components like Stack. However, to give better html semantics, as
prop would be the way to go: <Stack as="section"/>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I was thinking about the Box using as
in the wrong light. What I was thinking is that we get into a state where our components are loaded with boxes being converted to other components using as
, which would cause mental friction in trying to read through the code. Rather than looking at the tag name, you would have to look for the as
attribute to determine what it actually is.
That said, if this is relatively contained to Primer and not something we do in the consuming packages, that's probably fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any consideration for using sx
but recommending it be defined in a static scope, with nested classnames, etc being updated instead of the actual sx object?
I think that avoids consumers using styled-components directly, but also avoids the runtime overhead of sx object equality?
const styles = { | ||
display: 'inline-block', | ||
overflow: 'hidden', | ||
lineHeight: 'condensedUltra', | ||
borderRadius: getBorderRadius({size, square}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the intention is to require a dependency on merge @dusave but just that it's the consumers job to merge local and props sx if necessary?
if Box
is a Memo
component, could we avoid some overhead by doing a deeper comparison on sx props
memo(function Box(props) {...}, function shouldComponentRender(prevProps, nextProps) {
compare sx deepy, but maintain as object.is equality otherwise
})
which might avoid some/many performance differences from inline definitions of sx?
|
||
```tsx | ||
const Avatar: React.FC<AvatarProps> = ({size = 20, alt = '', square = false, sx = {}, ...props}) => { | ||
const styles = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we avoid styles
here, since it's very close to sounding like inline styles vs localSx
? or something like that
should we add types here too?
const localSx: BetterStyledSystemObject (is this still the type to use) = {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
componentStyles
or baseStyles
can be a better variable here. I don't want to prescriptive about the variable name in this ADR here because it depends on the context. (for example, Button has baseStyles
+ variantStyles
+ sizeStyles
)
BetterSystemStyleObject
is the correct type to use, however it could use some improvements as well. I'lld add that to the ADR. Example sandbox
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally! Just wanted to make sure we don't cause confusion with inline styles here, more than variable naming!
const styles = { | ||
display: 'inline-block', | ||
overflow: 'hidden', | ||
lineHeight: 'condensedUltra', | ||
borderRadius: getBorderRadius({size, square}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about the mental friction of implementing everything as a Box and transmuting them into other components by way of as. I always thought of that as an escape hatch more than a common pattern.
this, i think would be just something to extract to module scope and use from there, except when needing the props. how much non-primer usage actually changes based on props though?
Not yet, I only have that in a prototypes outside of primer/react 😅 I can see that happen in the future |
There are 2 common ways to create components with primer/react:
This ADR recommends using the 2nd one exclusively. See rendered markdown →
Next step after ADR is accepted: Update contributor docs.