Jeg har de siste årene jobbet mye med frontend applikasjoner skrevet i React og TypeScript. I de fleste tilfeller ender man opp med en ganske stor mengde komponenter i forskjellige størrelser. Det blir gjerne en del små elementer som knapper og input-felter, mer sammensatte greier som forms og layouts, og noen større side-komponenter. Her tenkte jeg å gå gjennom noen forskjellige måter å organisere CSS forhold til dette - og hva jeg foretrekker. Jeg jobber mye med Material UI (MUI) for tiden. Så eksemplene vil bruke MUI, men jeg vil tro man kan gjøre de samme valgene uansett hva man bruker for å skrive CSS. Jeg antar også at man har byggeverktøy som setter sammen CSS slik at det ikke er noen tekniske ulemper med å spre koden rundt i mange filer.

Global CSS

For 20 år siden var det gjerne dette vi brukte. En eller fler svære CSS filer som alle la til stiler i (og nesten ingen slettet), som var lagt til på hver HTML side. Så trakk man bare inn de klassene man trengte rundt om i layouten sin:

.formLayout {
  display: flex;
  gap: 16px;
  margin: 16px 0;
}

.buttonRow {
  display: flex;
  gap: 8px;
}

/* masse stiler.. */
<h4 class="formHeader">Some fancy form</h4>
<div class="formLayout">
  <input type="text" class="regularTextField" />
  <input type="number" class="smallTextField" />
</div>
<div class="buttonRow">
  <input type="button" class="mainButton" />
</div>

Dette viste seg raskt å være en skikkelig dårlig måte å gjøre ting på. Man får masse nesten like klasser, ubrukte ting som man glemmer å fjerne og generelt rot som er vanskelig å finne frem i. Og selv med nymotens ting som CSS variabler må man holde tunga bra rett i munnen om man skal få til en god struktur uten gjentagelser.

CSS på sammensatte komponenter

De fleste rammeverk tilbyr en eller annen måte å spre CSS-definisjoner rundt der den brukes. Et typisk eksempel fra MUI-verden vises nedenfor.

Vi kan definere stilene som en hook:

const useStyles = makeStyles((theme: Theme) => ({
  buttonRow: {
    display: "flex",
    gap: theme.spacing(),
  },
  formLayout: {
    display: "flex",
    gap: theme.spacing(2),
    margin: theme.spacing(2, 0),
  },
  smallInput: {
    width: theme.spacing(10),
  },
}));

Denne brukes gjerne i samme fil som der vi rendrer komponenten vår:

export default () => {
  const classes = useStyles();

  return (
    <div>
      <Typography variant="h4">
        Some fancy form
      </Typography>
      <div className={classes.formLayout}>
        <TextField
          variant="filled"
          label="Name"
        />
        <TextField
          variant="filled"
          label="Age"
          className={classes.smallInput}
        />
      </div>
      <div className={classes.buttonRow}>
        <Button color="secondary" variant="contained">Submit</Button>
        <Button>Cancel</Button>
      </div>
    </div>
  );
}

Ved første øyekast kan det se veldig likt ut som når man bruker globale CSS filer, men det er noen forskjeller. Viktigst for meg er at man kan definere stilene i samme fil som de skal brukes. Dette gjør det enklere å holde styr på hvilke stiler som faktisk brukes - så man kan unngå ubrukte stiler eller at man gjentar samme stil. Det er veldig raskt å finne frem og endre på ting - og man kan være trygg på at man ikke gjør endringer på andre komponenter som bruker samme stil. Om man bruker samme navn på en klasse i to forskjellige filer går fint - siden rammeverket fikser på navnene når man bygger applikasjonen.

Den store ulempen med denne måten å gjøre det på er at det er veldig raskt å duplisere stiler mellom relativt like komponenter. I eksempelet over her er det fort gjort at man ender med 100 nesten like utgaver av buttonRow eller formLayout rundt omkring i applikasjonen.

CSS på spesifikke komponenter

Hvis man i stedet lager en egen komponent av hvert element som trenger tilpasset stil får man mye større muligheter for gjenbruk. En måte å gjøre dette på kan se slik ut i MUI:

const ButtonRow = styled("div")(({theme}) => ({
  display: "flex",
  gap: theme.spacing(),
}));

const FormLayout = styled("div")(({theme}) => ({
  display: "flex",
  gap: theme.spacing(2),
  margin: theme.spacing(2, 0),
}));

const SmallInput = styled(TextField)(({theme}) => ({
  width: theme.spacing(10),
}));

Eller man kan bruke en slags inline styling om man liker det bedre:

const SmallInput = (props: TextFieldProps) => (
  <TextField
    sx={theme => ({
      width: theme.spacing(10),
    })}
    {...props}
  />
);

Da vil sammensatte komponenter ikke legge på egne stiler. Kun bruke ferdige komponenter som selv vet hvordan de skal tegnes ut:

export default () => {
  return (
    <div>
      <Typography variant="h4">
        Some fancy form
      </Typography>
      <FormLayout>
        <TextField
          variant="filled"
          label="Name"
        />
        <SmallInput
          variant="filled"
          label="Age"
        />
      </FormLayout>
      <ButtonRow>
        <Button color="secondary" variant="contained">Submit</Button>
        <Button>Cancel</Button>
      </ButtonRow>
    </div>
  );
}

Fordelen med dette er at man kanskje bruker den samme ButtonRow komponenten hver gang man trenger å tegne ut et sett med knapper. Så ser alle knapperader like ut - og det blir mye enklere å endre på en helhetlig måte.

Så hva skal man velge?

Jeg må innrømme at på prosjekter jeg har jobbet med ender det ofte opp med å være mye av disse sammensatte komponentene som definerer masse klasser selv. Dette er nok pga. at det er det raskeste å gjøre mens man utvikler. I noen tilfeller er det ok - det viktigste er at stilene er definert på samme sted som de brukes. På sikt tror jeg nok det hadde vært lurt å lage flere små komponenter som kan gjenbrukes. Så kan stiler i sammensatte komponenter forbeholdes enkle layouts og slikt som kun skal brukes ett sted.