gerrod
gerrod

Reputation: 6627

Using conditional styles in Material-UI with styled vs JSS

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.

Upvotes: 6

Views: 19676

Answers (2)

atazmin
atazmin

Reputation: 5687

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'}}>

Upvotes: 0

Ryan Cogswell
Ryan Cogswell

Reputation: 80996

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:

Upvotes: 5

Related Questions