TIL: DRYing Out Styles With styled-components
Tags: react, typescript, css
Published: 2021-05-04 23:12:38

---
title: TIL: DRYing Out Styles With styled-components
published: true
description: 
tags: react, typescript, css
series: TIL
//cover_image: https://direct_url_to_image.jpg
---

Welcome to the first post in my TIL series. My intention with this series is to share tidbits of neat things that I've learned, experimented with, or rediscovered.

This first post is going to be about extracting and reusing some common styling in React components using [styled-components](https://styled-components.com/) (a CSS-in-JS library for React), and how Typescript saved the day. 

So here's the background: I'm working in a React Typescript project that uses styled-components. In this project, there was a component that had a dropdown element to it and when the dropdown expanded a chevron svg would rotate as a bit of visual indication that the dropdown was now open. I needed to add a new component that had a dropdown element with a similar svg animation.

_Note: The [CodeSandbox sample project](https://codesandbox.io/s/sad-oskar-iz92p) isn't the actual project, but a stripped down example for this post._

## How I Started

Here's the first component. The interesting bit is the svg styling in the button, particularly the `transition` and `transform` properties. They combine to create the rotation animation that responds to the styled component prop change.

```jsx
// OriginalComponent.tsx
/* other components and stuff */

const DropdownTriggerButton = styled.button<DropdownTriggerButtonProps>`
  /* some button styles */
  svg {
    height: 1em;
    width: auto;
    /* LOOK HERE 👇 */
    transition: transform ease-in-out 300ms;
    ${(props) =>
      props.isOpen
        ? css`
            transform: rotate(0deg);
          `
        : css`
            transform: rotate(180deg);
          `}
  }
`;
```

## What Next?

I needed to add a new component, so I did that.

```jsx
// NewComponent.tsx
/* other components and stuff */

const Expander = styled.div<ExpanderProps>`
  svg {
    height: 1.5em;
    width: auto;
    cursor: pointer;
    /* LOOK HERE 👇 */
    transition: transform ease-in-out 200ms;
    ${(props) =>
      props.expanded
        ? css`
            transform: rotate(0deg);
          `
        : css`
            transform: rotate(180deg);
          `}
  }
`;
```

## The Neat Part

Through the use of the [css](https://styled-components.com/docs/api#css) utility in styled-components, I was able to extract the common svg animation to a reusable variable for inclusion in both components. *This is the DRYing (Don't Repeat Yourself) bit.*

```jsx
/* expandIconAnimation.tsx */
import { css } from "styled-components";

type expandIconAnimationProps = {
  expanded: boolean;
};

export const expandIconAnimation = css<expandIconAnimationProps>`
  svg {
    transition: transform ease-in-out 300ms;
    ${(props) =>
      props.expanded
        ? css`
            transform: rotate(0deg);
          `
        : css`
            transform: rotate(180deg);
          `}
  }
`;
```

This is neat for a couple reasons:
1. There's now an animation that's easy to include in new components that need it. We don't need to reinvent the wheel every time.
2. This promotes visual consistency across components. If the two components had the same icon animation for the same semantic reason (implemented separately), and there were differences in the timing function, or the animation duration, or even the transform, it wouldn't feel as cohesive. This can be a bad thing. If you did want to allow some style overrides for specific circumstances, you could change the `expandIconAnimation` variable to a function that accepted arguments for values that need to be be able to be overridden.

## Updating Our Original Components

Our components after being updated to use the `expandIconAnimation` "partial":

```jsx
const DropdownTriggerButton = styled.button<DropdownTriggerButtonProps>`
  /* some button styles */

  ${expandIconAnimation}
  svg {
    height: 1em;
    width: auto;
  }
`;
```

```jsx
const Expander = styled.div<ExpanderProps>`
  ${expandIconAnimation}
  svg {
    height: 1.5em;
    width: auto;
    cursor: pointer;
  }
`;
```

We've successfully reduced the duplication and ensured a consistent rotation animation for the expand/collapse functionality. However, you may have noticed that the `DropdownTriggerButton` used `isOpen` as the prop to control the transforms, whereas the `Expander` used `expanded`, and the `expandIconAnimation` expects `expanded`. This means that the usage in `DropdownTriggerButton` won't work. 

Luckily, Typescript catches that for us. When using `expandIconAnimation` in `DropdownTriggerButton` we get a build error that the property `expanded` is missing in the props type for `DropdownTriggerButton`, but is required by `expandIconAnimation`. Fortunately, it's a pretty simple fix in this scenario. We can just change the prop on `DropdownTriggerButton` from `isOpen` to `expanded`. The types are satisfied, and the animation works as expected. Thanks Typescript. 

## Takeaways
1. Making common styles can be a good way to reduce code duplication and promote visual consistency.
2. Typescript is a great tool to aid in the prevention of bugs.