Using component dot notation with TypeScript to create a set of components
👋 Hey! I’m no longer publishing new content on Medium. All new posts will be published on my personal site: https://skovy.dev. This post and all others are at https://skovy.dev/blog.
In a previous post and React meetup talk, I shared several patterns and tools for managing complex features with React and TypeScript. Many of the code samples were using component dot notation, and I briefly mentioned it but did not go in depth about the advantages of using this approach. This post will dive into the advantages of using component dot notation, highlight a few gotchas and provide some examples.
What is component dot notation?
As the name suggests, it’s using a “dot” to access the property of an object, more commonly referred to as dot notation. However, since this is at the component level (which are still just objects), I prefer “component dot notation” for clarity. A quick example of this is React Context. ThemeContext
is created and is the top-level component. Both the Provider
and Consumer
are then sub-components of ThemeContext
accessed using dot notation.
Definitions
These terms will be used throughout the remainder of the post.
- Top-level component: the actual component that is imported (eg:
ThemeContext
orFlex
). There is only one per set of components. - Sub-component: any component accessed using dot notation (eg:
ThemeContext.Provider
orFlex.Item
). There is one or more per set of components. - Component dot notation: accessing sub-components from a top-level component using dot notation.
Why use component dot notation?
There are three key benefits I’ve experienced when using component dot notation to both maintain and consume a set of components.
✏️ Namespacing
As a result of using component dot notation, all sub-components are inherently namespaced by the top-level component. Let’s take a Flex
component that wraps CSS flexbox as an example. The top-level component is named Flex
while it has only one sub-component, Flex.Item
.
It does not enforce or stop usage of using Flex.Item
outside of Flex,
but since it is a sub-component, it does imply to any developer that may be using it that it should only be used as a child of Flex
. Additionally, with this technique there is only a single entry point to use the flex components. It doesn’t matter if the Flex.Item
component definition and logic is in the same file as Flex
, in a sibling file or in a nested directory. The underlying implementation and file structure can be changed at any time because the only public contract is the export of Flex
. This drastically reduces the “public” API surface area as compared to importing every component individually where a change in implementation or file structure will break existing usages.
🚢 Single Imports
As mentioned above, there is only a single imported component which is elegant and clean. Once the top-level component has been imported (eg: import { Flex } from "flex"
) you never have to worry about adding or removing anything from this import statement. Depending on the number of components, this can save several lines at the top of the file. More importantly, as this feature evolves over time and pieces are added and removed due to changing requirements, the import remains entirely unchanged and reduces import noise in subsequent code reviews.
🔍 Discoverability
If there are “n” components in a set, a developer will have to memorize all “n” of those component names to know which to import or go file spelunking to find the component they actually need. However, with component dot notation, only the top-level component needs to be remembered and all component options will be suggested following the dot! There’s no need to memorize. This also improves discoverability of all components available that may not have been known.
Examples
There are various practical examples when component dot notation works well. For example, wrapper components like Flex
with Flex.Item
as a sub-component.
Or slightly more complex components in a design system that maybe have several building blocks, like a Table
component that has many sub-components such as Table.Row
,Table.Cell
, and Table.Head
that can be used as children only within Table
.
And lastly, as mentioned in the previous post, it works great for large or complex sets of components, like a Search
feature, which has a variety of filter components, pagination, results, etc.
Gotchas
There are two “gotchas” I have stumbled across when using component dot notation that are worth being aware of when using component dot notation.
Higher Order Components
It can be tricky using a higher order component, such connect
from react-redux
, on the top-level component. Specifically when using connect
, it will hoist all static attributes to the wrapping component (most higher order components do this), but the correct typings will not be preserved. In this case, the higher order component will need to be casted or more preferably, avoid using a higher order component with the top-level component.
Component Display Names
As discussed above, the underlying implementation of the sub-components does not matter. In the case of Flex
the Flex.Item
component implementation itself could be named NeverCallThisComponentDirectly
. This is great, but the only downside is that in React Devtools, it will be shown as NeverCallThisComponentDirectly,
which may be very confusing because it was never called directly.
One way around this is to set the displayName
on the component to match how it will be used. In this case, the component name remains NeverCallThisComponentDirectly
, but now has a display name of Flex.Item
.
Our underlying implementation has not changed at all, but now the component is both used as <Flex.Item />
and correctly seen in React Devtools as Flex.Item
.
Final thoughts
Component dot notation can be a useful technique when working with a set of components. It minimizes the API surface area to a single export, keeps the import simple and drastically improves the discoverability of available sub-components.