Building highly reusable React.js components using compound pattern
Tags: react, javascript, pattern
Published: 2020-12-23 03:08:47

Today I bring you a way to create a highly reusable React component using an advance pattern called **Compound**.

## Compound Components pattern
The keyword in the pattern’s name is the word **Compound**,  the word compound refers to something that is composed of two or more separate elements.

With respect to React components, this could mean a component that is composed of two or more separate components. The main component is usually called the **parent**, and the separate composed components, **children**.

Look at the following example:



![codeimg-twitter-instream-image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1608509165705/CDiIjW3Uw.png)

Here, `<Select>` is the **parent** component and the `<Select.Option>` are children components

The overall behaviour of a select element also relies on having these composed option elements as well. Hence, they are connected to one another.

The **state** of the entire component is managed by `Select` component with all `Select.Option` child components dependent on that state.

Do you get a sense of what compound components are now?

> Compound components are just one of many ways to express the API for your components.

We are going to build the `Select` component we saw above which will be composed of 2 additional components `Select Dropdown` and `Select Option`.

![component-mockup.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1608513848939/NpwJIlBJK.png)
In the code block above, you’ll notice I have used expressions like this: `Select.Option`

You can do this as well:

![codeimg-twitter-instream-image (2).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1608512608871/UnIIVBxt2.png)

Both work but it is a matter of personal preference. In my opinion, it communicates the dependency of the main component well, but that is just my preference.

> Feel free to use whatever component looks best to you!


## Building the compound child components
The `Select` is our main component, will keep track of the state, and it will do this via a boolean variable called **visible**.


```js
// select state 
{
  visible: true || false
}
``` 

The `Select` component needs to communicate the state to every child component regardless of their position in the nested component tree.

Remember that the children are dependent on the parent compound component for the state.

What would be the best way to do it?

We need to use the [React Context API](https://reactjs.org/docs/context.html) to hold the component state and expose the **visible** property via the **Provider** component. Alongside the **visible** property, we will also expose a string prop to hold the selected option **value**.

We’ll be creating this in a file called `select-context.js`

```js
import { createContext, useContext } from 'react'

const defaultContext = {
  visible: false,
  value: ''
};

export const SelectContext = createContext(defaultContext);

export const useSelectContext = () => useContext(SelectContext);
```

Now we have to create a file called `select-dropdown.js` which is the container for the select options.

> Note: I use  [Styled Components](styled-components.com) for the styles, feel free to use whatever styling way looks best to you!

```jsx
import React from "react";
import PropTypes from "prop-types";
import { StyledDropdown } from "./styles";

const SelectDropdown = ({ visible, children, className = "" }) => {
  return (
    <StyledDropdown visible={visible} className={className}>
      {children}
    </StyledDropdown>
  );
};

SelectDropdown.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  visible: PropTypes.bool.isRequired,
  className: PropTypes.string
};

export default SelectDropdown;
```


Next, we need to create a file called `styles.js` to save component styles.

```js
import styled, { css } from "styled-components";

export const StyledDropdown = styled.div`
  position: absolute;
  border-radius: 1.375rem;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
  background-color: #fff;
  max-height: 15rem;
  width: 80vw;
  overflow-y: auto;
  overflow-anchor: none;
  padding: 1rem 0;
  opacity: ${(props) => (props.visible ? 1 : 0)};
  visibility: ${(props) => (props.visible ? "visible" : "hidden")};
  top: 70px;
  left: 10px;
  z-index: 1100;
  transition: opacity 0.2s, transform 0.2s, bottom 0.2s ease,
    -webkit-transform 0.2s;
`;
```

> Note that with the `visible` property we control the visibility of the dropdown


Then we need to create the children component,  for this, we create a file called **select-option.js**.

```jsx
import React, { useMemo } from "react";
import { useSelectContext } from "./select-context";
import { StyledOption } from "./styles";
import PropTypes from "prop-types";


const SelectOption = ({
  children,
  value: identValue,
  className = "",
  disabled = false
}) => {
  const { updateValue, value, disableAll } = useSelectContext();

  const isDisabled = useMemo(() => disabled || disableAll, [
    disabled,
    disableAll
  ]);

  const selected = useMemo(() => {
    if (!value) return false;
    if (typeof value === "string") {
      return identValue === value;
    }
  }, [identValue, value]);

  const bgColor = useMemo(() => {
    if (isDisabled) return "#f0eef1";
    return selected ? "#3378F7" : "#fff";
  }, [selected, isDisabled]);

  const hoverBgColor = useMemo(() => {
    if (isDisabled || selected) return bgColor;
    return "#f0eef1";
  }, [selected, isDisabled, bgColor]);

  const color = useMemo(() => {
    if (isDisabled) return "#888888";
    return selected ? "#fff" : "#888888";
  }, [selected, isDisabled]);

  const handleClick = (event) => {
    event.preventDefault();
    if (typeof updateValue === "function" && identValue !== value) {
      updateValue(identValue);
    }
  };

  return (
    <StyledOption
      className={className}
      bgColor={bgColor}
      hoverBgColor={hoverBgColor}
      color={color}
      idDisabled={disabled}
      disabled={disabled}
      onClick={handleClick}
    >
      {children}
    </StyledOption>
  );
};

SelectOption.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  value: PropTypes.string,
  className: PropTypes.string,
  disabled: PropTypes.boolean
};

export default SelectOption;
```

I know it's confused, but I’ll break it down.

First, let's focus on the following line of code:

```
 const { updateValue, value, disableAll } = useSelectContext();
```

We use `useSelectContext()` from `select-context.js` to access the context data, "⚠️Spoiler alert": we are going to manage this data on our main component, Yes you are correct is the `Select` component.

The `value` prop from `context` is the selected value.

Also, we use `useMemo` on several occasions to prevent unnecessary renders.
```js
  const bgColor = useMemo(() => {
    if (isDisabled) return "#f0eef1";
    return selected ? "#3378F7" : "#fff";
  }, [selected, isDisabled]);

```

`useMemo` takes a callback that returns the `string` value with hexadecimal colour code and we pass an array dependency [selected, isDisabled]. This means that the memoized value remains the same unless the dependencies change.

> Note: If you have a theme you can use the `HOC` (High Order Component) component called `withTheme` and use your colours
 

Not sure how `useMemo` works? Have a look at this  [cheatsheet](https://react-hooks-cheatsheet.com/usememo).

Now to finalize the `SelectOption` component we need to create the `StyledOption` component for that we go to the `styles.js` file and write the following code:

```js
export const StyledOption = styled.div`
  display: flex;
  max-width: 100%;
  justify-content: flex-start;
  align-items: center;
  font-weight: normal;
  font-size: 1.3rem;
  height: 4rem;
  padding: 0 2rem;
  background-color: ${(props) => props.bgColor};
  color: ${(props) => props.color};
  user-select: none;
  border: 0;
  cursor: ${(props) => (props.isDisabled ? "not-allowed" : "pointer")};
  transition: background 0.2s ease 0s, border-color 0.2s ease 0s;
  &:hover {
    background-color: ${(props) => props.hoverBgColor};
  }
`;
```

## Creating the main component

Up to this point, we have all the child components of our main component, now we are going to create the main component `Select`, for that we need to create a file called `select.js` with the following code: 

```jsx
import React, { useState, useCallback, useMemo, useEffect } from "react";
import { SelectContext } from "./select-context";
import { StyledSelect, StyledValue, StyledIcon, TruncatedText } from "./styles";
import SelectDropdown from "./select-dropdown";
import { pickChildByProps } from "../../utils";
import { ChevronDown } from "react-iconly";
import PropTypes from "prop-types";

const Select = ({
  children,
  value: customValue,
  disabled = false,
  onChange,
  icon: Icon = ChevronDown,
  className,
  placeholder = "Choose one"
}) => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState(undefined);

  useEffect(() => {
    if (customValue === undefined) return;
    setValue(customValue);
  }, [customValue]);

  const updateVisible = useCallback((next) => {
    setVisible(next);
  }, []);

  const updateValue = useCallback(
    (next) => {
      setValue(next);
      if (typeof onChange === "function") {
        onChange(next);
      }
      setVisible(false);
    },
    [onChange]
  );

  const clickHandler = (event) => {
    event.preventDefault();
    if (disabled) return;
    setVisible(!visible);
  };

  const initialValue = useMemo(
    () => ({
      value,
      visible,
      updateValue,
      updateVisible,
      disableAll: disabled
    }),
    [visible, updateVisible, updateValue, disabled, value]
  );

  const selectedChild = useMemo(() => {
    const [, optionChildren] = pickChildByProps(children, "value", value);
    return React.Children.map(optionChildren, (child) => {
      if (!React.isValidElement(child)) return null;
      const el = React.cloneElement(child, { preventAllEvents: true });
      return el;
    });
  }, [value, children]);

  return (
    <SelectContext.Provider value={initialValue}>
      <StyledSelect
        disabled={disabled}
        className={className}
        onClick={clickHandler}
      >
        <StyledValue isPlaceholder={!value}>
          <TruncatedText height="4rem">
            {!value ? placeholder : selectedChild}
          </TruncatedText>
        </StyledValue>
        <StyledIcon visible={visible}>
          <Icon />
        </StyledIcon>
        <SelectDropdown visible={visible}>{children}</SelectDropdown>
      </StyledSelect>
    </SelectContext.Provider>
  );
};

Select.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  disabled: PropTypes.bool,
  icon: PropTypes.element,
  value: PropTypes.string,
  placeholder: PropTypes.string,
  onChange: PropTypes.func,
  className: PropTypes.string
};

export default Select;

```

I will start by explaining the propTypes:


- `children`: Are the array of `Select.Option`
- `disabled`: Is used to set the disabled state in `Select` and `Select.Option`
- `value`: Is the default selected value 
- `placeholder`: Is used to show a text if there aren't any `Select.Option` selected.
- `onChange`: Callback to communicate when the value has changed
- `className`: Class name for `Select` component

Perfect now let's focus on the `useState` React hook, it's used to manage selected value status and dropdown menu visibility

```js
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState(undefined);
```

To set the initial value of `Select` (in case one is set), we need to use the hook `useEffect`
```js
  useEffect(() => {
    if (customValue === undefined) return;
    setValue(customValue);
  }, [customValue]);

```
> Not sure how `useEffect` works? Have a look at this  [cheatsheet](https://react-hooks-cheatsheet.com/useeffect). 

```js
  const updateVisible = useCallback((next) => {
    setVisible(next);
  }, []);

  const updateValue = useCallback(
    (next) => {
      setValue(next);
      if (typeof onChange === "function") {
        onChange(next);
      }
      setVisible(false);
    },
    [onChange]
  );
```

Another hooks we are using is `useCallback`, this hook will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

> Not sure how `useCallback` works? Have a look at this  [cheatsheet]https://react-hooks-cheatsheet.com/usecallback). 

Now we are going to focus on context initial value, let's see following code: 

```jsx
  const initialValue = useMemo(
    () => ({
      value,
      visible,
      updateValue,
      updateVisible,
      disableAll: disabled
    }),
    [visible, updateVisible, updateValue, disabled, value]
  );

return (
    <SelectContext.Provider value={initialValue}>
     // ---- ///
    </SelectContext.Provider>
  );
```

In the above code, we use the `useMemo` to prevent unnecessary re-renders passing in the array the props that can change, then we pass that initial value to the`SelectContect.Provider`, we have been using each of these properties in the components we saw earlier.

Last but not least, we have a function to get selected option component, let's see following code:

```js
export const pickChildByProps = (children, key, value) => {
  const target = [];
  const withoutPropChildren = React.Children.map(children, (item) => {
    if (!React.isValidElement(item)) return null;
    if (!item.props) return item;
    if (item.props[key] === value) {
      target.push(item);
      return null;
    }
    return item;
  });

  const targetChildren = target.length >= 0 ? target : undefined;

  return [withoutPropChildren, targetChildren];
};

 const selectedChild = useMemo(() => {
    const [, optionChildren] = pickChildByProps(children, "value", value);
    return React.Children.map(optionChildren, (child) => {
      if (!React.isValidElement(child)) return null;
      const el = React.cloneElement(child, { preventAllEvents: true });
      return el;
    });
  }, [value, children]);
```

In a few words, what we do is clone the selected option and put it in the header of the `Select` component.

Now we need to create the necessary styles for the `Select` component:

```js
export const StyledSelect = styled.div`
  position: relative;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  user-select: none;
  white-space: nowrap;
  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
  width: 80vw;
  transition: border 0.2s ease 0s, color 0.2s ease-out 0s,
    box-shadow 0.2s ease 0s;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
  border: 2px solid #f5f5f5;
  border-radius: 3rem;
  height: 4rem;
  padding: 0 1rem 0 1rem;
  background-color: ${(props) => (props.disabled ? "#f0eef1" : "#fff")};
  &:hover {
    border-color: ${(props) => (props.disabled ? "#888888" : "#3378F7")};
  }
`;

export const StyledIcon = styled.div`
  position: absolute;
  right: 2rem;
  font-size: ${(props) => props.size};
  top: 50%;
  bottom: 0;
  transform: translateY(-50%)
    rotate(${(props) => (props.visible ? "180" : "0")}deg);
  pointer-events: none;
  transition: transform 200ms ease;
  display: flex;
  align-items: center;
  color: #999999;
`;

export const StyledValue = styled.div`
  display: inline-flex;
  flex: 1;
  height: 100%;
  align-items: center;
  line-height: 1;
  padding: 0;
  margin-right: 1.25rem;
  font-size: 1.3rem;
  color: "#888888";
  width: calc(100% - 1.25rem);
  ${StyledOption} {
    border-radius: 0;
    background-color: transparent;
    padding: 0;
    margin: 0;
    color: inherit;
    &:hover {
      border-radius: inherit;
      background-color: inherit;
      padding: inherit;
      margin: inherit;
      color: inherit;
    }
  }
  ${({ isPlaceholder }) =>
    isPlaceholder &&
    css`
      color: #bcbabb;
    `}
`;
```

> Feel free to change the colours, you can use the `theme` object of `styled-components` to get the theme colours

Finally, we need to export our component 👏🏻 

```js

import Select from "./select";
import SelectOption from "./select-option";

// Remember this is just a personal preference. It's not mandatory
Select.Option = SelectOption;

export default Select;
```


Congratulations! 🎊, now you have a reusable highly optimized component created, you can apply this pattern in many cases.

## Final result

Here you can see the final result:
{% codesandbox react-compound-pattern-73rx6 %}