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 (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 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.
// 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.
// 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 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.
/* 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:
- 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.
- 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":
const DropdownTriggerButton = styled.button<DropdownTriggerButtonProps>`
/* some button styles */
${expandIconAnimation}
svg {
height: 1em;
width: auto;
}
`;
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
- Making common styles can be a good way to reduce code duplication and promote visual consistency.
- Typescript is a great tool to aid in the prevention of bugs.
Top comments (0)