Prop Drilling, Component Composition, Types & API

Props & Components in React

Yesterday I learned some new concepts in React.

Let's start by:

Prop Drilling

You know you're Prop Drilling when you're passing props deep down the nested component tree. The intermediary child components of the parent component end up receiving the props that they don't even need and are only acting like a middleman to pass the prop further down the nested tree.

Sometimes it's fine to pass props down one to three levels down the nested tree but it can get worse when you're passing props down to five to ten levels deep.

This also creates another where intermediary components end up receiving too many props making it harder to understand the component itself.

The way to fix this problem is through:

Component Composition

The Component Composition is an idea in which you pass other components as props you can do this directly (which is often the preferred way) by setting a prop name or indirectly by enclosing the child components inside the parent component and using the children's prop to receive the component.

The Component Composition also makes it easy to reuse a component. Let's say you create a box and put a content component inside it. You can't reuse that component of the box to pass other content because you embedded the content inside the box component.

But with Component Composition it acts as if you created a hole inside a component and you can pass other components to fill that hole thus making the component highly reusable.

function NonReuseableBox() {
  return (
    <div className="box">
      <Content />
    </div>
  );
}

function IndirectReuseableBox({ children }) {
  return <div className="box">{children}</div>;
}

function DirectReuseableBox({ element }) {
  return <div className="box">{element}</div>;
}

function App() {
  return (
    <>
      <NonReuseableBox />
      <IndirectReuseableBox>
        <Content />
      </IndirectReuseableBox>
      <IndirectReuseableBox>
        <DifferentContent />
      </IndirectReuseableBox>
      <DirectReuseableBox element={<Content />} />
      <DirectReuseableBox element={<DifferentContent />} />
    </>
  );
}

Another interesting I learned is how to make:

Component API

Whenever making a component we need to think about it as someone who makes the component: the component creator and someone who uses the component: the component consumer

Even if both are you. But there will be a time when you're working with a team and you need to build a highly usable component.

You need to strike a good balance in how many props you provide to customize the component. Too few props make the component not really useable and sometimes useless and too many make it complicated and hard to use.

Along with providing props, you need to set good default values such that a component is easy to use. Users may want more control over the style and you can provide them a prop to add additional style class to the component.

If you're using state in prop then you make sure you provide a state setter function that receives the value of the state inside the component such that users can use that to manage the state in their app.

Let's say you make a slider component that allows the user to set a value and you're using state to store the value. Now if a user is using that component then how can they figure out what the current value is? If they can't then your component is useless to them. They can't perform any operation based on the slider value.

Your component is a presentational component to them. This brings us to our next concept:

Components Types

Components can be classified into various types but in a broader sense they are of three types: Presentational/Stateless Components, Stateful Components & Structural Components

  1. Presentational/Stateless Components: These components don't do anything in and of themselves they are just there for presentation purposes. They don't have any state in them and receive data through props thus we can also call them Stateless Components.

  2. Stateful Components: These components have state (data) and related logic inside them thus we can also call them Stateful Components.

  3. Structural Components: These components are there to provide structure. These structural components can be huge or small. For example, the App component contains the whole app, whereas the Nav component may contain the logo, search, and nav links. They are different from the above component types because they only provide structure for these components to be inside of them.

Splitting Components

You may start out writing your app component but naturally, you will split it into different sub-components.

Just like we discuss component props having many too-small components may be useless and having too large components may be too complex and hard to understand due to complexity.

You want to find a balance here as well while splitting large components into small sub-components. You know you need to split the component when:

  1. You're passing too many props
  2. Your component is becoming exceedingly large
  3. You're getting confused
  4. You're thinking things don't belong together and it's logical they need to be separated for better clarity

Adding Type-Checking

Let's get back to our component API.

In React you can add static type-checking to make sure your user of the component doesn't pass values that are not acceptable. For example, let's say your component can accept color prop then you don't want your user to pass a number as value. You want to provide warnings to your user to avoid bugs in the codebase.

For this type of type-checking, React has a built-in native type-checking. But if your project is large it's preferred to use TypeScript instead of JavaScript to do these things.

To add type-checking you have to import it and then add it to your component (see below example).

Here is the whole component I wrote for demonstrating what we learn by far about component API:

import { useState } from "react";
import PropTypes from "prop-types";

Slider.propTypes = {
  count: PropTypes.number,
  defaultCount: PropTypes.number,
  className: PropTypes.string,
  color: PropTypes.string,
  size: PropTypes.number,
  message: PropTypes.array,
  onSetRange: PropTypes.func,
};

export default function Slider({
  count = 5,
  defaultCount = 0,
  className = "",
  color = "#4c8bf5",
  size = 24,
  message = ["Avoid", "Bad", "Average", "Good", "Amazing"],
  onSetRange = () => {},
}) {
  const [range, setRange] = useState(defaultCount);

  const wholeSliderStyle = {
    display: "flex",
    gap: ".8rem",
    color,
    fontSize: `${size}px`,
    lineHeight: 0,
  };

  const sliderStyle = {
    display: "flex",
    alignItems: "center",
  };

  return (
    <div style={wholeSliderStyle}>
      <input
        style={sliderStyle}
        type="range"
        value={range}
        min="0"
        max={count}
        className={className}
        onChange={(e) => {
          setRange(e.target.value);
          onSetRange(e.target.value);
        }}
      />
      <p>
        {message.length === count
          ? message[(range > 0 && range) - 1]
          : range > 0 && range}
      </p>
    </div>
  );
}

I do like to mention it's okay to set the initial value of the state using props, the only time you should not do this is when you want to keep the state in sync with the prop, which means when the prop value changes the state should change as well.

And that's not what we're doing here. We are only providing initial value to our state.

That's it!

Thanks for reading!

Follow me: @wasimapinjari

Links: wasimapinjari.bio.link

Subscribe to stay up-to-date with my latest articles.

Subscribe