React inheritance vs. composition pattern

Viewed 272

(This is more of a philosophical / pedagogical question, not a practical one.)

I'm try to explain the benefits of composition over inheritance in React. Immediately I imagined an inheritance hierarchy that would break:

class Border extends React.Component {}

class ColorBorder extends Border {}

class DashedBorder extends Border {}

class DashedColorBorder extends ??? {}

And then I thought, "Hah! This will be easy to achieve with composition!"

[EDIT] The rest that follows distracts from the core question: "How do you most effectively demonstrate how composition in React rocks where inheritance would fall apart?" That's the question I'm most keen about. [/EDIT]

But interestingly I got stuck just the same:

const Border = ({ children }) => (
  <div className="Border">
    {children}
  </div>
)

const ColorBorder = ({ color, children }) => (
  <Border style={{ borderColor: color }}>
    {children}
  </Border>
)

const DashedBorder = ({ children }) => (
  <Border style={{ borderStyle: 'dashed' }}>
    {children}
  </Border>
)

const DashedColorBorder = ({ color, children }) => (
  // how to use both ColorBorder and DashedBorder?
)

From a practical standpoint, the solution is easy: just extend Border to accept more props and don't bother with any of this nonsense. But I'm curious if there is a way to do it, for the sake of argument.

It left me wondering if it's not a great example of where composition dominates inheritance, and if there's a better more obvious example out there. Most articles talk about React composition in imprecise terms, quickly diving into HOCs and such. Further, they don't even try to explain React using inheritance and where it starts to fall over. I always want to figure out "why not?" People take it on faith that composition is better, and I'm looking for examples of how.

The best I could come up with was this, but I'm curious how others might tackle it. I'm dissatisfied with how you still have to provide a manually in these examples:

const Border = ({ children, style }) => (
  <div className="Border mb-3" style={style}>
    {children}
  </div>
)

const ColorBorder = ({ color, children }) => (
  React.Children.map(children, child => (
    React.cloneElement(child, {
      style: { ...child.props.style, borderColor: color },
    })
  ))
)

const DashedBorder = ({ children }) => (
  React.Children.map(children, child => (
    React.cloneElement(child, {
      style: { ...child.props.style, borderStyle: 'dashed' },
    })
  ))
)

const DashedColorBorder = ({ color, children }) => (
  <DashedBorder>
    <ColorBorder color={color}>
      {children}
    </ColorBorder>
  </DashedBorder>
)

export const App = () => (
  <div className="App">
    <Border>
      Hello world
    </Border>

    <ColorBorder color="green">
      <Border>
        Hello world
      </Border>
    </ColorBorder>

    <DashedBorder>
      <Border>
        Hello world
      </Border>
    </DashedBorder>

    <DashedColorBorder color="red">
      <Border>
        Hello world
      </Border>
    </DashedColorBorder>
  </div>
)
2 Answers

To avoid using cloneElement, you could use context instead as below, though in practice this is still a much bigger pain than simply making a stand-alone <DashedColorBorder> component. I've added a <ThickBorder> component as well for demonstration purposes.

import React, { useContext } from "react";

const StyleContext = React.createContext({});

const Border = ({ style, children }) => (
  <div
    className="Border"
    style={{
      ...useContext(StyleContext),
      ...style // keep if you want to be able to override style directly
    }}
  >
    {children}
  </div>
);

const ColorBorder = ({ color, innerMost = true, children }) => {
  return (
    <StyleContext.Provider value={{ ...useContext(StyleContext), 
     borderColor: color }}>
      {innerMost ? <Border>{children}</Border> : children}
    </StyleContext.Provider>
  );
};

const DashedBorder = ({ innerMost = true, children }) => {
  return (
    <StyleContext.Provider value={{ ...useContext(StyleContext), 
     borderStyle: "dashed" }}>
      {innerMost ? <Border>{children}</Border> : children}
    </StyleContext.Provider>
  );
};

const ThickBorder = ({ innerMost = true, children }) => {
  return (
    <StyleContext.Provider value={{ ...useContext(StyleContext), 
     borderWidth: "5px" }}>
      {innerMost ? <Border>{children}</Border> : children}
    </StyleContext.Provider>
  );
};

const DashedColorBorder = ({ color, children }) => (
  <DashedBorder innerMost={false}>
    <ColorBorder color={color}>{children}</ColorBorder>
  </DashedBorder>
);

const ThickDashedColorBorder = ({ color, children }) => (
  <ThickBorder innerMost={false}>
    <DashedColorBorder color={color}>{children}</DashedColorBorder>
  </ThickBorder>
);

export default function App() {
  return (
    <div className="App">
      <Border>Hello world</Border>
      <ColorBorder color="green">Hello world</ColorBorder>
      <DashedBorder>Hello world</DashedBorder>
      <DashedColorBorder color="red">Hello world</DashedColorBorder>
      <ThickDashedColorBorder color="blue">Hello world</ThickDashedColorBorder>
    </div>
  );
}

CodeSandbox demo

I adapted your code:

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

const Border = ({ children, style }) => (
  <div className="Border mb-3" style={{ borderStyle: "solid", ...style }}>
    {children}
  </div>
);

const ColorBorder = ({ color, children }) => (
  <Border style={{ borderColor: color }}>{children}</Border>
);

const DashedBorder = ({ children }) => (
  <Border style={{ borderStyle: "dashed" }}>{children}</Border>
);

const DashedColorBorder = ({ color, children }) => (
  <Border style={{ borderColor: color, borderStyle: "dashed" }}>
    {children}
  </Border>
);

const App = () => (
  <div className="App">
    <Border>Hello world</Border>

    <ColorBorder color="blue">Hello world</ColorBorder>

    <DashedBorder>Hello world</DashedBorder>

    <DashedColorBorder color="red">Hello world</DashedColorBorder>
  </div>
);

export default App;

My general advice is to avoid inheritance and cloneElement, the later unless absolutely necessary.

At Facebook, we use React in thousands of components, and we haven’t found any use cases where we would recommend creating component inheritance hierarchies.

Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions.

https://reactjs.org/docs/composition-vs-inheritance.html

Related