How best to create custom styled components in MUI - reactjs

seemingly simple problem:
Example Background: we have many lists of items and render them in the UI with <List>.
Example Problem: we usually (but not always) want to remove the extra padding that is injected on <List> (ex: <UnpaddedList>).
As another example, perhaps we want to have most lists be collapsible by default with a <List onClick={...}> trigger to become <CollapsableList>. For most examples below, the unpadded is sufficient to illustrate.
On first guess, I'd expect something like CustomList extends List (but see Facebook inheritance advice). There are many ways to do it, but what are the advantages/disadvantages...they seem to conflict:
createStyled()
This is intended to entirely replace the theme... in times intended to break brand standards. Although we could go back and merge the existing theme, this seems against the purpose of this tool, and deepmerge perhaps could slow performance? this would work well if it could inherit the current theme context. perhaps lots of extra component names is bad practice? ex: List, UnpaddedList UnpaddedLargeList, UnpaddedLargeGreenList, etc.
see https://mui.com/system/styled/#create-custom-styled-utility
createTheme()
this allows total override of styles and props for each component. The only issue is that it may not be clear why a component is styled a certain way and keeps its name. (ex: <List> isn't renamed to <UnpaddedList>)This could be confusing if you have a component that is inheriting a theme from a parent component somewhere up in the tree and appears with an unexpected style. the developer would have to trace each parent component to find where the theme was injected with <ThemeProvider>. But perhaps such injecting sub-themes in unexpected ways would be its own anti-pattern.
Also, without clear docs/typescript, it took a lot of reading to determine how best to pass and modify the current theme:
function extendThemeWithGreen(theme: Theme) {
let themeOptions: ThemeOptions = { palette: { primary: { main: "green" } } };
return createTheme(theme, themeOptions);
}
function Example() {
return <ThemeProvider theme={extendThemeWithGreen}> example</ThemeProvider>;
}
another issue is that some components have arbitrary white-space injected into them. this is not documented and has no type-hinting. the only way to discover those is to hunt down the source code. from there you would likely have to create another set of styles that override the native styles and introduce bloat into the app.
see https://mui.com/system/styled/#custom-components
see https://mui.com/customization/how-to-customize/#2-reusable-style-overrides
return <List {...props} />
Wrapping components would be nice. (ex: const UnpaddedList = (props) => <List disablePadding {...props} />;)
so far this has been a lot of trouble. for example, should the props be ListProps or OverridableComponent<ListTypeMap<{}, "ul">>? Or suppose both the end component and wrapped component have sx... do I have to set a deepMerge to deal with duplicate props. Maybe I just need to spend more time to get it going?
see https://mui.com/guides/composition/#wrapping-components
sx={...} or style={...} or class=...
these props generally allow customization, however they are effectively one-off. ex: you have to <List style={{padding:0}} /> also, this loses type safety (expect things to break in the future) class can be separate, but still hard to retain type safety. Legacy makeStyles( is similar
see https://mui.com/system/the-sx-prop/
see https://mui.com/styles/api/#createstyles-styles-styles
<UnstyledList component={...}
This is largely an upcoming feature and not fully implemented yet. Although this could avoid future css conflicts, it still doesn't facilitate props override (or if it does... it isn't clear from the docs)
see https://mui.com/customization/unstyled-components/#main-content
<Box component={List} ... />
Use the Box wrapper to create a custom component. The docs aren't entirely clear on this. It may be something like:
function UnpaddedList(props: ListProps){
return <Box component={List} disablePadding {...props} />
}
Many of the component props have generic modifiers... however it isn't clear how this new component would be written to match (or extend) the original component props
see https://mui.com/system/box/#overriding-mui-components
variant="unpadded"
this is close, however it appears to create a typing issue, or special treatment to import. i could see this causing confusion down the road, but at least linting would throw a warning if not implemented properly. it doesn't have a good way to do composition (ex: variant="unpadded&collapsible"). it also isn't clear how this is done for components that do not have variants built in.
see https://mui.com/customization/theme-components/#adding-new-component-variants
props
of course some, but not all, can be done through props keep type safety. (ex: <List disablePadding={true}>) however this isn't DRY. it is overall worse to manage (no brand standard). it also makes the code much more difficult read and excessively verbose
other methods?
perhaps I missed another way. I also can't find any graceful solution to the hardcoded whitespace in MUI components

If you have a component that is used in multiple places and only one of them needs to have some special styles or you need to tweak or fix an edge case in a specific layout, use sx prop. It's suitable for one-off styles like this:
<List sx={{ p: 0 }}>
If you need to apply some styles occasionally in a component, use styled. For example if a component is used in 100 places but only in 20 places it needs to have the padding disabled then add a padding prop to enable the styles in the rarer case:
const options = {
shouldForwardProp: (prop) => prop !== 'disablePadding',
};
const List = styled(
MuiList,
options,
)(({ theme, disablePadding = false }) => ({
...(disablePadding && {
padding: 0,
}),
}));
// I dont always use disablePadding but sometimes I need it
<List disablePadding>

Related

Should I test all component props?

For example, I have component like this:
const Button = ({borderColor, children}) => {
return (
<button style={{borderColor: borderColor ? `border: 1px solid ${borderColor}` : null}}>
{children}
</button>
)
}
Main goals of this component are:
pass children prop further
handle custom border color
With children - it's usual situation. But with css I don't know, should I test it, or not.
Some articles about RTL tell that we don't have to test css. But
in my opinion, in this case, our css prop is important for end user
(e.g. some other developer who will use this component) and this css
should be tested. What's the right way?
Testing a React Application should be about behaviors. That is, how the application behaves when controlled by an end user. That's one of the guiding principles of react-testing-library, and one I agree with.
Therefore, going by this statement you can do two different things:
Test the complete behavior that causes the border color to change. That is Integration Testing
Type check the component to guarantee that you cannot pass an incorrect value for borderColor. That is Unit Testing
In my opinion with this use case, testing anything else would be testing React itself or, as the other answer noted, you ability to write a correct css string. The later can be tested with your own eyes anyway and isn't likely to change
Type checking example
Giving you an example for integration testing is hard to do without knowing your complete use case. As for type checking you have two options:
Using PropTypes
Switching to Typescript
Note: These options aren't equivalent. Proptypes is a library that checks the props at runtime with a developement build. They are ignored with a production build. Typescript on the other hand is a complete compiler, that runs during the build. The correct solution depends on your setup
With PropTypes
You can use PropTypes.oneOf :
Button.propTypes = {
// Edit as needed
borderColor: PropTypes.oneOf(["red", ,blue"])
}
Typescript
interface ButtonProps {
borderColor: "blue" | "red";
}
const Button: React.FunctionComponent<ButtonProps> = ({borderColor, children}) => {...}
Potentially misreading your question, but maybe you're only asking because "is this string a valid CSS color" feels like a silly or annoying detail to test, even though you're aware of the problems that not writing tests will have on other developers who will use this component.
Is this a silly thing to test? I agree conditionally.
IMO this is far better solved using Typescript to enforce constraints on what form the string can take. Some ideas here: TypeScript - is it possible validate string types based on pattern matching or even length?
A better thing to test is probably a functionality question like "does this component genuinely create a button wrapping the children with the 1px solid borderColor that we asked for?
In that case, yes I'd write a test for that functionality.

Avoid magic strings when working with Chakra Ui

A problem, I keep running into with Chakra Ui are "magic strings". Let's look at an example of muted text:
function Example() {
const color = useColorModeValue("gray.400", "gray.200");
return (
<Text color={color} />
)
}
What's the problem with this code: In my app, I want to share the muted color between many components. However, defining it explicitly as a string means that I have to remember that muted text has a value of "gray.400". If I have another component that wants to use muted text, I have to copy the string "gray.400" to all other components. I will end up with lots of strings that make it really hard to change things across the entire app. I explored two solutions so far:
Solution 1 - TextStyles API: Chakra comes with a textStyles API out of the box but this doesn't work well for more complicated situations (what if I want to have a hover and active state with different colors?).
Solution 2 - Create a global object: I've created a hookuseConsistantStyles() that returns a theme-like object with values, e. g.: {"borderLight": "gray.200"}. However, this feels like I'm fighting the library.
I'd really love to have a better solution since I keep running into this.
This is probably late but for any new onlookers, since Styled System v1.17.0, chakra ui has semantic tokens. The changelog and docs have some details but the important bit is:
Semantic tokens provide the ability to create css variables which can change with a CSS condition.
CSS conditions would be states like dark mode or hover etc.
In this case, you would define a semantic token like so (with a better token name):
const customTheme = extendTheme({
semanticTokens: {
colors: {
nicelyNamedToken: {
default: 'gray.400',
_dark: 'gray.200',
},
},
},
})
import the theme however you currently do, and then anywhere the theme is present, you should be able to just use the token
function Example() {
return (
<Text color='nicelyNamedToken' />
)
}
I don't think Solution 2 is actually that bad -- but I agree that it feels like everything should be accessible since it already exists.
If you're using a chakra theme, you can actually import the theme (i.e: import { theme } from "#chakra-ui/react"
And then you can start accessing things off of that object, like colors (t is the theme import)
Note: you still have to 'know' the keys on the object, but this is at least an existing, consistent dictionary that you can reference.
I have not tested this with different color themes, etc.

Arrow function vs Component React

I recently made a pull request at my company and got feedback on some code that I had written and I wanted some other opinions on this.
We have an component called Icon that can take another component as a prop like so:
<Icon component={ArrowDown}/>
this simply renders the following:
<IconContainer>
<ArrowDown/>
</IconContainer>
Now you can also do the follow if you need to create a custom icon:
<Icon component={()=><div>custom Icon</div>}/>
The reviewer commented that the function ()=><div>custom Icon</div> should be removed outside the scope for performance reasons to prevent re-rendering:
const CustomIcon = ()=><div>custom Icon</div>
const someComponent = ()=><Icon component={customIcon}/>
I'm not convinced that this will improve performance (code-readability sure) but wanted to get some other opinions on it.
Thanks!
Arrow functions are anonymous and will be re-instantiated on every render.
If you create a named component, it will have a reference and will not be re-rendered by React unless and until required (through state update).
And also, as you mentioned, it provides better readability and an option for code splitting.

Customizing children components' styles through parent with JSS

I'm currently working on a project which has all of its styles declared in JSS. One of the "benefits" highlighted on many articles and the library docs is that it encapsulates styles. However I really am having a hard time customizing them, specially when it comes to styling that depends on the component's surrounding context (by context I mean parent elements, siblings, etc.).
Consider the following styles exported along a component called FieldDescriptor via the withStyles HOC:
info: {
fontFamily: theme.typography.fontFamily.light,
fontSize: "12px",
padding: "0 24px 8px 24px",
letterSpacing: 0.3,
},
This class will be found as FieldDescriptor-info-xxx on the element having that class. Now suppose that this component is child to another one that attempts to target the error class. You could target that class with something like [class*=FieldDescriptor-error] (personally I already consider this a very unclean approach) and it will work only on a development environment.
On production, classes will become unique (e.g. jss-xxx) and selectors like the one above will no longer be useful. My question is, what is the cleanest or "correct" approach to customizing component styles like this in JSS? I am either missing something really obvious or perhaps facing the limitations of JSS.
I am looking forward to solutions that do not require more tooling or code bloating, that would really miss the purpose of adopting JSS in the first place.
You can find an example using both withStyles and useStyles here
Try to think of a component as of a black box that intentionally hides implementation details from the outside world.
With this model, only component itself is responsible for it's presentation, so you need to ask that component to change something. In React world you do that most of the time by passing props to that component.
Inside of that component you can go multiple ways, combining the predefined classes depending on the props (preferred because static) or using function rules/values which let you access the props and define the styles per component instance.

Are there any benefits of calling an SFC function within the render method?

I am working with the react-gsap library, and want to encapsulate specific <Tween> instances that do not need any props, because they are occuring multiple times in the same way.
Lets take a simple example of an instance, that doesn't need any props or state at all. Let's say this line
<Tween
to={{opacity: 0}}
duration={5}
/>
occurs really often in our code base, and we want to abstract it.
My first idea was to just create an SFC for that:
const HideTween = () => (
<Tween to={{ opacity: 0 }} duration={5} />
);
// and then ...
<HideTween />
but that seems not to work at all. The Tweens are simply not showing any effect.
I then came up with another idea (which i personally dislike) to just call the SFC. Instead of <HideTween /> , we now have
{HideTween()}
and voila, it works...
I am specifically curious now, why my first idea did not work at all. The question is not aimed into the gsap library directly, but more of a general form: Where, when and how can such an approach (of abstracting parts of your render into own functions) fail? If it is the library, how does it even achieve such a behaviour?
And why does it seem to work, when i call the function directly (i know this is kind of a bad approach)? Without any state or props present,
shouldn't <HideTween /> have the same effect on every render as {HideTween()}?
EDIT
Here is a minimal example
Exchange <Tweens /> in line 38 with the content of the Tweens SFC, and you will see the animation again.
You can share the same elements between different render methods by creating them ahead of the render cycle as you tried to do:
const HideTween = <Tween to={{ opacity: 0 }} duration={5} />
and then directly using the element (not instance) in the render methods:
<Timeline>
{HideTween}
</Timeline>
<Component /> is JSX sugar for creating an element of a react component. So your first idea didn't work as it would return an element returning an element of a Tween and not directly the Tween element. For usual DOM rendering this will work as expected, though. The second example works as you get the Tween element this way, but you're right to dislike it as it simply adds an unnecessary indirection.
Doing it this way you might think that react will reuse the same instance of the component in different places, but it will actually instantiate the component anew for each usage. Here is an example:
https://stackblitz.com/edit/react-rzq5q5?file=index.js
I assume this is intentional as sharing the same instance of a component seems to be quite a rare use case and with stateful components the state would be shared as well synchronising components in different parts of the app (which could end up being quite confusing).
Also check out this article on the difference between components, elements and instances (instances will be created by react for you): React Components, Elements, and Instances
Here I've set up a simple example trying out a few things: Example
In your Tween component:
class Tween extends React.Component {
static defaultProps = {
to: {opacity: 0},
duration: 5
}
...
}

Resources