Compound.Components and Context
Tags: react
Published: 2019-02-18 07:06:54

---
title: Compound.Components and Context
published: true
description: The combination of the dot syntax and context can make a component smart while keeping composition intact at the same time.
tags: react
---

> This post was sent on [my newsletter](https://sid.studio/newsletter) a few weeks ago, just saying.
>
> This is part of a series of posts about [writing good components](https://sid.studio/component-api)


Today, let's talk about forms.

You have probably read a bunch of articles about state management in forms, this isn't one of them. Instead, I'd like to talk about the design and API of forms.

<example>

![form with label on left](https://sid.studio/blog/29/label-on-left.png)

</example>

There are a bunch of things happening here, but first let me show you the API.

```jsx
<Form layout="label-on-left">
  <Form.Field label="Name">
    <TextInput type="text" placeholder="Enter your name" />
  </Form.Field>

  <Form.Field label="Email">
    <TextInput
      type="email"
      placeholder="email@domain.com"
    />
  </Form.Field>
</Form>
```

Let's look at each one of the components and break them down:

&nbsp;

### Form

It starts with a `Form` component which on the surface is a basic form element with a class attached. It renders whatever you put inside it.

```jsx
function Form(props) {
  return <form className="form">{props.children}</form>
}

render(<Form layout="label-on-left">...</Form>)
```

It also accepts a `layout` prop, which is useful for use cases where you are short on space.

<example class="cut-bottom">

![form with label on top](https://sid.studio/blog/29/label-on-top-phone.png)

```jsx
<Form layout="label-on-top">...</Form>
```

</example>

This changes how the labels are aligned (right to left) and how margins work.

The form doesn't control the width and margin of it's inner contents though. That's for the form field to take care of.

The `Form` does need to communicate this layout information down though.

The easiest thing to do would be to pass the layout in `props`, but because the contents of a form is dynamic (decided by the developer who uses this form), we can't control that.

This is where the context API comes in.

```jsx
/* Create a new context */
const LayoutContext = React.createContext()

function Form(props) {
  /*
    Wrap the children in a context provider
    with the value based on props
  */
  return (
    <form className="form">
      <LayoutContext.Provider
        value={{ layout: props.layout }}
      >
        {props.children}
      </LayoutContext.Provider>
    </form>
  )
}

export default Form
export { LayoutContext }
```

Now a form field can consume this context and get the value of `layout`

### Form Field

The `FormField` component adds a `label` before rendering whatever you put inside it (like a text input).

```jsx
function Field(props) {
  return (
    <div className="form-field">
      <label {...props}>{props.label}</label>
      {props.children}
    </div>
  )
}
```

In addition to that, it adds a class for layout - which comes from the context we created in `Form`.

```jsx
/* Get the layout consumer */
import { LayoutContext } from './form'

/*
  Use the consumer to get access to the
  context - it uses the render prop API

  We pass this as the class to the form field
*/
function Field(props) {
  return (
    <LayoutContext.Consumer>
      {context => (
        <div className={`form-field ${context.layout}`}>
          <label {...props}>{props.label}</label>
          {props.children}
        </div>
      )}
    </LayoutContext.Consumer>
  )
}
```

The `useContext` hook from React 16.8+ makes the syntax easier to understand

```jsx
/* Get the layout consumer */
import { LayoutContext } from './form'

function Field(props) {
  /*
    Grab the context from the useContext hook
    which accepts the context variable as the input
  */
  const context = useContext(LayoutContext)

  return (
    <div className={`form-field ${context.layout}`}>
      <label {...props}>{props.label}</label>
      {props.children}
    </div>
  )
}
```

If you're curious, you can check the css here:

```css
.form-field.label-on-left {
  max-width: 625px;
  display: flex;
  align-items: center; /* align vertically */
}
.form-field.label-on-left label {
  text-align: right;
  width: 175px;
  margin-right: 25px;
}

.form-field.label-on-top {
  width: 100%;
  display: block; /* instead of flex*/
}
.form-field.label-on-top label {
  text-align: left; /* instead of right */
  margin-bottom: 25px; /* instead of margin-right */
}
```

### Form.Field?

The last detail I want to talk about is this awkward dot syntax for components.

Because the form `Field` is always used with a `Form`, it makes sense to group them together for the user.

One way to do that is to export it from the same file:

```jsx
/* form.js */
import Field from './field'

function Form(props) {
  /* ... */
}
export default Form

export { Field }
```

Now, users can import them together:

```jsx
import Form, { Field } from 'components/form'

render(
  <Form>
    <Field>...</Field>
  </Form>
)
```

We can make a tiny improvement by attaching the `Field` on the `Form` component itself.

```jsx
/* form.js */
import Field from './field'

function Form(props) {
  /* ... */
}

Form.Field = Field
export default Form
```

This code works because React components are javascript objects, and you can add additional keys on this object.

What this means for the user is when they import `Form`, they get the `Field` automatically.

```jsx
import Form from 'components/form'

render(
  <Form>
    <Form.Field>...</Form.Field>
  </Form>
)
```

I really like this API because it makes the connection between `Form` and `Form.Field` obvious.

##### Note: You have to move the context into a different file to avoid cyclic dependency loop.

The combination of the dot syntax and context makes our `Form` component smart while keeping it composable at the same time.

Hope that was helpful in your journey

Sid

---

[![newsletter](https://sid.studio/newsletter-snapshot.png?v=4)](https://sid.studio/newsletter)