Using conditional styles in Material-UI with styled vs JSS
Asked Answered
B

2

6

I'm using Material-UI v5 and trying to migrate to using styled instead of makeStyles because it seems as though that's the "preferred" approach now. I understand using makeStyles is still valid but I'm trying to embrace the new styling solution instead.

I've got a list of list items which represent navigation links, and I want to highlight the one that's currently selected. Here's how I did this using makeStyles:

interface ListItemLinkProps {
    label: string;
    to: string;
}

const useStyles = makeStyles<Theme>(theme => ({
    selected: {
        color: () => theme.palette.primary.main,
    },
}));

const ListItemLink = ({ to, label, children }: PropsWithChildren<ListItemLinkProps>) => {
    const styles = useStyles();

    const match = useRouteMatch(to);
    const className = clsx({ [styles.selected]: !!match });

    return (
        <ListItem button component={Link} to={to} className={className}>
            <ListItemIcon>{children}</ListItemIcon>
            <ListItemText primary={label} />
        </ListItem>
    );
};

(Note here I'm using clsx to determine if the selected style should be applied to the ListItem element.)

How do I achieve this using styled? This is what I've come up with so far (note: the interface for ListItemLinkProps hasn't changed so I haven't repeated it here):

const LinkItem = styled(ListItem, {
    shouldForwardProp: (propName: PropertyKey) => propName !== 'isSelected'
})<ListItemProps & LinkProps & { isSelected: boolean }>(({ theme, isSelected }) => ({
    ...(isSelected && { color: theme.palette.primary.main }),
}));

const ListItemLink = ({ to, label, children }: PropsWithChildren<ListItemLinkProps>) => {
    const match = useRouteMatch(to);

    return (
        // @ts-ignore
        <LinkItem button component={Link} to={to} isSelected={!!match}>
            <ListItemIcon>{children}</ListItemIcon>
            <ListItemText primary={label} />
        </LinkItem>
    );
};

So, two questions about this:

  1. Is this best way to do a conditional style?

  2. The other problem is that I can't work out the correct types for the styled declaration - I have to put the // @ts-ignore comment above the LinkItem because of the way its types are declared.

Bigford answered 6/8, 2021 at 0:34 Comment(0)
T
5

Material-UI v5 uses Emotion for the default style engine and consistently uses styled internally in order to make it easier for people who want to use styled-components instead of Emotion to not have to include both in the bundle.

Though the styled API works fine for a lot of use cases, it seems like a clumsy fit for this particular use case. There are two main options that provide a considerably better DX.

One option is to use the new sx prop available on all Material-UI components (and the Box component can be used to wrap non-MUI components to access the sx features). Below is a modification of one of the List demos demonstrating this approach (with the custom ListItemButton simulating the role of your ListItemLink):

import * as React from "react";
import Box from "@material-ui/core/Box";
import List from "@material-ui/core/List";
import MuiListItemButton, {
  ListItemButtonProps
} from "@material-ui/core/ListItemButton";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Divider from "@material-ui/core/Divider";
import InboxIcon from "@material-ui/icons/Inbox";
import DraftsIcon from "@material-ui/icons/Drafts";

const ListItemButton = ({
  selected = false,
  ...other
}: ListItemButtonProps) => {
  const match = selected;
  return (
    <MuiListItemButton
      {...other}
      sx={{ color: match ? "primary.main" : undefined }}
    />
  );
};
export default function SelectedListItem() {
  const [selectedIndex, setSelectedIndex] = React.useState(1);

  const handleListItemClick = (
    event: React.MouseEvent<HTMLDivElement, MouseEvent>,
    index: number
  ) => {
    setSelectedIndex(index);
  };

  return (
    <Box sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}>
      <List component="nav" aria-label="main mailbox folders">
        <ListItemButton
          selected={selectedIndex === 0}
          onClick={(event) => handleListItemClick(event, 0)}
        >
          <ListItemIcon>
            <InboxIcon />
          </ListItemIcon>
          <ListItemText primary="Inbox" />
        </ListItemButton>
        <ListItemButton
          selected={selectedIndex === 1}
          onClick={(event) => handleListItemClick(event, 1)}
        >
          <ListItemIcon>
            <DraftsIcon />
          </ListItemIcon>
          <ListItemText primary="Drafts" />
        </ListItemButton>
      </List>
      <Divider />
      <List component="nav" aria-label="secondary mailbox folder">
        <ListItemButton
          selected={selectedIndex === 2}
          onClick={(event) => handleListItemClick(event, 2)}
        >
          <ListItemText primary="Trash" />
        </ListItemButton>
        <ListItemButton
          selected={selectedIndex === 3}
          onClick={(event) => handleListItemClick(event, 3)}
        >
          <ListItemText primary="Spam" />
        </ListItemButton>
      </List>
    </Box>
  );
}

Edit SelectedListItem Material Demo

The only downside of this approach is that it is currently notably slower than using styled, but it is still fast enough to be fine for most use cases.

The other option is to use Emotion directly via its css prop. This allows a similar DX (though not quite as convenient use of the theme), but without any performance penalty.

/** @jsxImportSource @emotion/react */
import * as React from "react";
import Box from "@material-ui/core/Box";
import List from "@material-ui/core/List";
import MuiListItemButton, {
  ListItemButtonProps
} from "@material-ui/core/ListItemButton";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Divider from "@material-ui/core/Divider";
import InboxIcon from "@material-ui/icons/Inbox";
import DraftsIcon from "@material-ui/icons/Drafts";
import { css } from "@emotion/react";
import { useTheme } from "@material-ui/core/styles";

const ListItemButton = ({
  selected = false,
  ...other
}: ListItemButtonProps) => {
  const match = selected;
  const theme = useTheme();
  return (
    <MuiListItemButton
      {...other}
      css={css({ color: match ? theme.palette.primary.main : undefined })}
    />
  );
};
export default function SelectedListItem() {
  const [selectedIndex, setSelectedIndex] = React.useState(1);

  const handleListItemClick = (
    event: React.MouseEvent<HTMLDivElement, MouseEvent>,
    index: number
  ) => {
    setSelectedIndex(index);
  };

  return (
    <Box sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}>
      <List component="nav" aria-label="main mailbox folders">
        <ListItemButton
          selected={selectedIndex === 0}
          onClick={(event) => handleListItemClick(event, 0)}
        >
          <ListItemIcon>
            <InboxIcon />
          </ListItemIcon>
          <ListItemText primary="Inbox" />
        </ListItemButton>
        <ListItemButton
          selected={selectedIndex === 1}
          onClick={(event) => handleListItemClick(event, 1)}
        >
          <ListItemIcon>
            <DraftsIcon />
          </ListItemIcon>
          <ListItemText primary="Drafts" />
        </ListItemButton>
      </List>
      <Divider />
      <List component="nav" aria-label="secondary mailbox folder">
        <ListItemButton
          selected={selectedIndex === 2}
          onClick={(event) => handleListItemClick(event, 2)}
        >
          <ListItemText primary="Trash" />
        </ListItemButton>
        <ListItemButton
          selected={selectedIndex === 3}
          onClick={(event) => handleListItemClick(event, 3)}
        >
          <ListItemText primary="Spam" />
        </ListItemButton>
      </List>
    </Box>
  );
}

Edit SelectedListItem Material Demo

In the app I work on (which I haven't yet started to migrate to v5), I expect to use a combination of styled and Emotion's css function/prop. I'm hesitant to use the sx prop heavily until its performance improves a bit (which I think will happen eventually). Even though it performs "fast enough" for many cases, when I have two options with similar DX available and one is twice as fast as the other, I find it difficult to opt for the slower one. The main cases where I would opt for the sx prop are for components where I want to set CSS properties differently for different breakpoints or similar areas where the sx prop provides much nicer DX than other options.

Related answers:

Thenceforward answered 6/8, 2021 at 3:54 Comment(2)
Thank you so much for the comprehensive answer!Bigford
Thanks Ryan. Helpful as I've just migrated to V5 with gradual migration mods. Some changes during migration: breakpoints, overrides and CSS Class name changesOverlay
B
0

This seem to work for me with using sx

import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';

...

const isMobile = useMediaQuery(useTheme().breakpoints.down('md'));

...

<Divider sx={{ whiteSpace: isMobile ? 'normal' : 'pre'}}>
Bornu answered 28/1, 2022 at 17:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.