Andrei Dobrinski's Blog

React Component Composition with Dot Notation Exports

Intended Audience

Beginner and intermediate React developers looking to learn more about the dot notation export pattern with React components.

Example

We’ll take a look at what a modal component might look like with the dot notation export pattern. The demo uses React with Styled Components.

Let’s jump right into the code.

// Modal.js
import styled from 'styled-components'

export const Button = styled.button.attrs(() => ({ type: 'button' }))`
  // button styles
`

export const Heading = styled.h2`
  // heading styles
`

export const Body = styled.p`
  // body styles
`

export const Wrap = styled.div`
  // outer modal styles
`

Modal.Heading = Heading;
Modal.Body = Body;
Modal.Button = Button;

export function Modal({ isVisible, children }) {
  return (
    <Wrap isVisible={isVisible}>
      {children}
    </Wrap>
  )
}

---

// ParentComponent.js
import { useState } from 'react'
import { Modal } from '../components/Modal'

export function ParentComponent() {
  const [isModalVisible, setModalVisible] = useState(false)

  return (
    <>
      <Modal isVisible={isModalVisible}>
        <Modal.Header>Hello, I am a Modal</Modal.Header>
        <Modal.Body>I can show helpful information</Modal.Body>
        <Modal.Button onClick={() => setModalVisible(false)}>
          Hide Modal
        </Modal.Button>
      </Modal>
      <button
        onClick={() => setModalVisible(true)}
        type="button"
      >
        Show Modal
      </button>
    </>
  )
}

Let’s now go over what’s happening in this example.

Breakdown

The Modal component and some styled components are co-located and exported from Modal.js. We then use Modal.Heading = Heading pattern, for example, to allow for Heading to be used with the Modal export.

The parent component imports only Modal. The styled components may be used inside the Modal, depending on what’s needed in the implementation, and they can be called with the dot notation pattern.

There are a few benefits to using this kind of pattern.

Benefits

This pattern is useful when multiple components are often composed and used together. This can be useful because:

  • Inversion of control: the user, and not the Modal, is not responsible for use cases

Let’s say we were to build a version of the modal component that accepts props in place of children. Imagine it looks something like this.

// Modal.js

export function Modal({ isVisible, header, body, buttonText, buttonOnClick }) {
  return (
    <Wrap isVisible={isVisible}>
      {header && <Header>{header}</Header>}
      {body && <Body>{body}</Body>}
      {button && <Button onClick={buttonOnClick}>{buttonText}</Button>}
    </Wrap>
  )
}

// ParentComponent.js
import { useState } from 'react'
import { Modal } from './Modal'

export function ParentComponent() {
  const [isModalVisible, setModalVisible] = useState(false)

  return (
    <>
      <Modal
        isVisible={isModalVisible}
        header="Hello, I am a Modal"
        body="I am receiving this data as props"
        buttonText="Ok"
        buttonOnClick={() => console.log('button clicked')}
      />
    </>
  )

This version of the modal is resposible for handling all the use cases from the props it gets, as well as the conditional rendering of components in cases where props are missing.

Let’s say that the Modal’s use cases get extended: a new implementation calls for an <a> tag instead of a button and an icon above the header text. The prop-based modal would need to accept these new props and add logic to handle all the use cases.

The composed version of the modal can simply expose a <Modal.Icon> and <Modal.Link> and let the user of the modal build out the use case, and the Modal component will render its children. The Modal inverts the control by letting the the user handle the use case, instead of handling it internally.

  • Fewer import statements are needed to implement Modal and its children

import { Modal } from '../components/Modal instead of import { Modal, Heading, Button, Body } from '../components/Modal'

  • Co-locating Modal with the components it’s commonly used with

Keeping the Modal and all of its potential children in the same file for each on maintenance.

  • The JSX communicates that Modal and its children are related

For example, we can more easily infer that these components are related

<Modal>
  <Modal.Heading>Heading</Modal.Heading>
  <Modal.Body>Body</Modal.Body>
  <Modal.Button>Button</Modal.Button>
</Modal>

than these components

<Modal>
  <Heading>Heading</Heading>
  <Body>Body</Body>
  <Button>Button</Button>
</Modal>
  • TypeScript can help autocomplete the components

If you start typing <Modal., TypeScript will show a list of the components you’ve assigned to Modal. The autocomplete helps with productivity (typing components faster) and discovery (showing which components are available).

React’s children prop makes for component composition and dot notation is something you may be familiar with, especially from using open source libraries. React Navigation’s Stack.Screen comes to mind as well as React’s own API import pattern of

import * as React from 'react'

// React.useState, React.createContext, React.lazy etc.

Alternatives

Without changing any of the code, we can just as easily import and use the components in Modal.js separately, without dot notation.

// ParentComponent.js
import { useState } from 'react'
import { Modal, Heading, Button, Body } from '../components/Modal'
export function ParentComponent() {
  const [isModalVisible, setModalVisible] = useState(false)

  return (
    <>
      <Modal isVisible={isModalVisible}>
        <Header>Hello, I am a Modal</Header>        <Body>I can show helpful information</Body>        <Button onClick={() => setModalVisible(false)}>Hide Modal</Button>      </Modal>
      <button onClick={() => setModalVisible(true)} type="button">
        Show Modal
      </button>
    </>
  )
}

We can also choose not to co-locate the rest of the components with Modal and import them from elsewhere. This might be a good option if the Button needs to be used somewhere other than in the Modal. This option isn’t mutually exclusive with the dot notation export, as Modal.js can still import the component it needs and re-export it to use with dot notation.

// Modal.js
import { Button } from './Button'

Modal.Button = Button

export function Modal({ children }) {
  return (
    <div>
      Modal
      {children}
    </div>
  )
}

The component that imports Modal could then call Modal.Button just like in the above example.

Edit on GitHub

Back to All Posts
Built with ❤️  in 🇨🇦