React SubComponent Pattern - acl-services/paprika GitHub Wiki

Context

In Paprika one of our signature patterns is the SubComponent Pattern. This is when you use a component inside of a parent component to compose reusable UI pieces.

This pattern has some benefits:

  • Makes using our components similar to how you would use regular HTML.
  • You only have to import one component which will include all of its variations.
  • It is easier to understand as you can split functionality across subcomponents.
  • Allows you to keep your APIs smaller, cleaner and simpler by splitting props across multiple components.

What does this pattern look like?

There are three variations for this pattern:

1. Wrapping other components inside of a parent component

import Button from "@paprika/Button";

...
return (<Button.Icon />)

Real example: @paprika/button

How to implement

Exposing a subcomponent beneath its parent is fairly easy; simply add the subcomponent as a property of the parent. Example:

import React from "react";
import SubComponent from "./components/SubComponent";

export default Parent(props) {
  return <React.Fragment>Something</React.Fragment>
}

// you can expose your SubComponent with whatever name you might like.
Parent.SubComponent = SubComponent; // now it's exposed

//App.js
import Parent from "@paprika/parent";

export default App() {
  return (
    <React.Fragment>
      <Parent /> {/* Use the parent */}
      <Parent.SubComponent /> {/* or the subComponent */}
    </React.Fragment>
  )
}

2. Composing your parent component with different subcomponents

import ListBox from "@paprika/listbox";

...
return (
  <ListBox>
    <ListBox.Filter />
    <ListBox.Option>Cat</ListBox.Option>
    <ListBox.Option>Dog</ListBox.Option>
    <ListBox.Option>Hamster</ListBox.Option>
  <ListBox>
);

Real examples: @paprika/listbox @paprika/popover

How to implement

In case you are using a subcomponent to be composable, the best approach it's to use a Context to communicate the parent component and the subcomponent. This has the added benefit of making the subcomponent adaptable to be used in other components.

// Parent.js
import React from "react";
import SubComponent from "./SubComponent";
// create a context and export it
export const ContextParent = React.createContext(null);

export default function Parent(props) {
  const [isSelected, setIsSelected] = React.useState([]);
  const { children } = props;

  return (
    <ContextParent.Provider value={{ isSelected, setIsSelected }}>
      {children}
    </ContextParent.Provider>
  );
}

Parent.SubComponent = SubComponent;


// SubComponent.js
import React from "react";

import { ContextParent } from "./Parent";

export default function SubComponent(props) {
  const { index } = props;
  const { isSelected, setIsSelected } = React.useContext(ContextParent);

  const handleClick = index => () => {
    setIsSelected(isSelectedArray => {
      // it's an example
      // we don't care about duplicate indexes or toggling the state
      return [...isSelectedArray, index];
    });
  };

  return (
    <button onClick={handleClick(index)}>
      {index} {isSelected.includes(index) ? "👋" : ""}
    </button>
  );
}

// App.js
import React from "react";
import Parent from "./Parent";

export default function App() {
  return (
    <Parent>
      <Parent.SubComponent index="0" />
      <Parent.SubComponent index="1" />
      <Parent.SubComponent index="2" />
    </Parent>
  );
}

CodeSandBox Example

3. Passing down props data via a subcomponent

import DataGrid from '@paprika/datagrid';

...
return (
  <DataGrid>
    <DataGrid.ColumnDefinition header={} cell={} />
    <DataGrid.ColumnDefinition header={} cell={} />
    <DataGrid.ColumnDefinition header={} cell={} />
  </DataGrid>
);

Real example: @paprika/data-grid

How to implement

For this case using Context will be not enough. We need a process that extracts each of the subcomponents so we can access their props and then internally manipulate those to create our desired UI output.

To do that in our library we make use of the displayName property to detect the kind of the component which contains the parent component. There are alternative ways for doing the same but for us displayName has worked just fine so far.

To extract the subcomponent we have been using a helper function in our library:

extractChildren.js

import React from "react";

export default function extractChildren(children, types) {
  const _children = [];
  const components = {};
  if (Array.isArray(types)) {
    React.Children.toArray(children).forEach(child => {
      if (child.type && types.includes(child.type.displayName)) {
        if (Object.prototype.hasOwnProperty.call(components, child.type.displayName)) {
          const childs = Array.isArray(components[child.type.displayName])
            ? [...components[child.type.displayName], child]
            : [components[child.type.displayName], child];

          components[child.type.displayName] = childs;
        } else {
          components[child.type.displayName] = child;
        }
      } else {
        _children.push(child);
      }
    });

    return { ...components, children: _children };
  }

  throw new Error("extractChildren types parameter must be an Array");
}

Example.

// Parent.js
import React from "react";
import SubComponent from "./SubComponent";
import extractChildren from "./extractChildren";

export default function Parent(props) {
  const { "Parent.SubComponent": parentSubComponents } = extractChildren(
    props.children,
    ["Parent.SubComponent"]
  );

  return parentSubComponents.map((component, index) => {
    const { name, lastName } = component.props;
    return (
      <div>
        {index}: {name} - {lastName}
      </div>
    );
  });
}

Parent.SubComponent = SubComponent;

// SubComponent.js

/** SubComponent
you can declare your propTypes for this component if you want to
This subComponent work as a proxy for our props s a shallow component
*/

export default function SubComponent(props) {
  return null;
}

/* IMPORTANT PART will let us discover the SubComponent in our parent */
SubComponent.displayName = "Parent.SubComponent";

// App
import React from "react";
import "./styles.css";
import Parent from "./Parent";

export default function App() {
  return (
    <div className="App">
      <Parent>
        <Parent.SubComponent name="Ronaldo" lastName="Nazário de Lima" />
        <Parent.SubComponent name="Ronaldinho" lastName="Gaucho" />
      </Parent>
    </div>
  );
}

CodeSandBox Example

Caveats

There are scenarios where it might be tempting to have a third subComponent or more <Parent.SubComponent.Subcomponent />. While this is possible, it might not be pleasing for the eyes of the developer that it's consuming it.

So instead, split the component into <Parent /> and <SubComponent.SubComponent />.

So the developing experience looks like:

import Parent, { SubComponent } from "@paprika/parent";

return (
  <Parent>
    <SubComponent.SubComponent />
    <SubComponent.SubComponent />
  </Parent>
)

The end 🌶

⚠️ **GitHub.com Fallback** ⚠️