๐Ÿ“ฆ hshoff / composing-components

a tutorial for those that are curious

โ˜… 1 stars โ‘‚ 0 forks ๐Ÿ‘ 1 watching โš–๏ธ MIT License
๐Ÿ“ฅ Clone https://github.com/hshoff/composing-components.git
HTTPS git clone https://github.com/hshoff/composing-components.git
SSH git clone git@github.com:hshoff/composing-components.git
CLI gh repo clone hshoff/composing-components
Harrison Shoff Harrison Shoff [readme] add tutorial 0c498a0 9 years ago ๐Ÿ“ History
๐Ÿ“‚ master View all commits โ†’
๐Ÿ“„ LICENSE
๐Ÿ“„ README.md
๐Ÿ“„ README.md

A silly little tutorial on composing React components

For those that are curious

The Problem

Let's say you have some view that looks like so:

// ./SomeView.js

import React from 'react';
import IconLabelValueRow from './IconLabelValueRow';
import EditLabelRow from './EditLabelRow';

function SomeView(props) {
  return (
    <div className='some-view'>
      <div className='space-top-2 space-4'>
        <IconLabelValueRow
          iconClass='icon-beach icon-doorman'
          label='Some Label'
          value='Some Value'
        />
      </div>
      <div className='space-top-8 space-4'>
        <EditValueRow label='Something' />
      </div>
    </div>
  );
}

We see a common pattern of wrapping our components in div with some positioning classes (they apply top & bottom margins).

<div className='space-top-2 space-4'>
  // <IconLabelValueRow />
</div>
<div className='space-top-8 space-4'>
  // <EditValueRow />
</div>

One way to solve it

One solution would be add pass a spacing value through props and add the value to the className on IconLabelValueRow.

// ./IconLabelValueRow.js

import React from 'react';
import cx from 'classnames';

function IconLabelValueRow(props) {
  const {
    iconClass, label, value,
    spaceTop, spaceBottom
  } = props;

  const classes = cx({
    [`space-top-${spaceTop}`]: !!spaceTop,
    [`space-${spaceBottom}`]: !!spaceBottom,
  }, 'flexbox flexbox--row flexbox--align-center');

  return (
    <div className={classes}>
      <div><i className={`icon ${iconClass}`} /></div>
      <div>{label}</div>
      <div>{value}</div>
    </div>
  );
}

Then SomeView would look like this:

// ./SomeView.js

function SomeView(props) {
  return (
    <div className='some-view'>
      <IconLabelValueRow
        iconClass='icon-beach icon-doorman'
        label='Some Label'
        value='Some Value'
        spaceTop={2}
        spaceBottom={8}
      />
      <div className='space-top-8 space-4'>
        <EditValueRow label='Something' />
      </div>
    </div>
  );
}

Another problem

But this doesn't solve the problem for EditValueRow. We would have to copy and paste the spaceTop and spaceBottom props preamble over to the other component like so:

// ./EditLabelRow.js

import React from 'react';
import cx from 'classnames';

export default function EditLabelRow(props) {
  const { label, spaceTop, spaceBottom } = props;

  const classes = cx({
    [`space-top-${spaceTop}`]: !!spaceTop,
    [`space-${spaceBottom}`]: !!spaceBottom,
  }, 'flexbox');

  return (
    <div className={classes}>
      <input placeholder={label} />
    </div>
  );
}

So now SomeView looks like this:

// ./SomeView.js

function SomeView(props) {
  return (
    <div className='some-view'>
      <IconLabelValueRow
        iconClass='icon-beach icon-doorman'
        label='Some Label'
        value='Some Value'
        spaceTop={2}
        spaceBottom={8}
      />
      <EditValueRow
        label='Something'
        spaceTop={8}
        spaceBottom={4}
      />
    </div>
  );
}

But copy + paste makes us sad so how do you decorate your component in a composable way?

A better solution

Enter higher-order functions. A higher-order function takes a component and in this example returns a state-less function.

// ./enhancers/Spacing.js

import React from 'react';
import cx from 'classnames';

export default function Spacing(Component) {
  return (props) => {
    const { spaceTop, spaceBottom } = props;
    const classes = cx({
      [`space-top-${spaceTop}`]: !!spaceTop,
      [`space-${spaceBottom}`]: !!spaceBottom,
    });

    return (
      <div className={classes}>
        <Component {...props} />
      </div>
    );
  }
}

And now we can use this in IconLabelValueRow.

// ./IconLabelValueRow.js

import React from 'react';
import Spacing from './enhancers/Spacing';

function IconLabelValueRow(props) {
  const { iconClass, label, value } = props;
  return (
    <div className='flexbox flexbox--row flexbox--align-center'>
      <div><i className={`icon ${iconClass}`} /></div>
      <div>{label}</div>
      <div>{value}</div>
    </div>
  );
}

/**
 * only difference is we export a composed component
 */
export default Spacing(IconLabelValueRow);

and in EditLabelRow:

// ./EditLabelRow.js

import React from 'react';
import Spacing from './enhancers/Spacing';

function EditLabelRow(props) {
  const { label } = props;
  return (
    <div>
      <input placeholder={label} />
    </div>
  );
}

export default Spacing(EditLabelRow);

And now we live in a happy world.

// ./SomeView.js

import IconLabelValueRow from './IconLabelValueRow';
import EditLabelRow from './EditLabelRow';

function SomeView(props) {
  return (
    <div className='some-view'>
      <IconLabelValueRow
        iconClass='icon-beach icon-doorman'
        label='Some Label'
        value='Some Value'
        spaceTop={2}
        spaceBottom={4}
      />
      <EditValueRow
        label='Something'
        spaceTop={8}
        spaceBottom={4}
      />
    </div>
  );
}