DEV Community

Cover image for Build Decoupled React Components with Inversion of Control
Smilepk
Smilepk

Posted on

Build Decoupled React Components with Inversion of Control

Imagine you’re implementing an e-commerce checkout, and the product manager has temporally chosen PayPal as payment provider. But it’s planned to change the provider for another one, e.g.: Stripe. Looking for small changes in the future, how would you implement this?
Inversion of Control (IoC) probably will help you in this case.
What is IoC?
IoC is a programming principle where you invert the control flow of a program, it’s used to decouple your code. You can read more about here, let’s go code.

You have the checkout.component.tsx that is responsible for creating the checkout flow. It renders the product list, total of payment and the payment form that is provided by PayPal.


interface Props {}

const CheckoutComponent = (props: Props) => {
  const [products, setProducts] = useState([...products]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const total = useMemo(() => ...total, []);

  const onPayError = () => { ... };

  const onPaySuccess = () => { ... };

  const onPay = () => {
     paypal.someMethod(onPaySuccess, onPayError);
  };

  // All the payment stuff goes here

  return (
    <div>
      <div>
        Products:
        <ul>
          {products.map(product => <li key={product.id}>{product.name} - U${product.price}</li>)}
        </ul>
        <p>Total: U${total}</p>
      </div>
      <PaypalForm />
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

The problem here is that your checkout flow knows about PayPal, and that can generate a huge refactor in the future.
To prevent this, you can use ElementType and interfaces.


interface ProviderProps {
  onPaymentSucceed: () => void;
  onPaymentError: (error: string) => void;
  onPaymentStart: () => void;
  ... any prop here
}

interface ComponentProps {
  provider: JSX ElementConstructor<ProviderProps>;
}


Enter fullscreen mode Exit fullscreen mode

ProviderProps ensure that any provider will implement the correct methods. ComponentProps forces the checkout flow to receive a provider and mount it, passing the given props.
Now we can create a “dumb” component that knows how to control the screen state, like loading and feedback.


const CheckoutComponent = ({ provider: Provider }: ComponentProps) => {
  const [products, setProducts] = useState([...products]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const total = useMemo(() => ...total, []);

  const onPaymentError = (error: string) => setError(error);

  const onPaymentSucceed = () => redirectToHome();

  const onPaymentStart = () => setLoading(true);

  return (
    <div>
      <div>
        Products:
        <ul>
          {products.map(product => <li key={product.id}>{product.name} - U${product.price}</li>)}
        </ul>
        <p>Total: U${total}</p>
      </div>
      <Provider
        onPaymentStart={onPaymentStart}
        onPaymentSucceed={onPaymentSucceed}
        onPaymentError={onPaymentError}
      />
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

And the responsible for managing the payment with PayPal can be isolated, totally decoupling the code.


const PayPalProvider = ({ onPaymentSucceed, onPaymentError, onPaymentStart }: ProviderProps) => {
  const handlePayment = () => {
    onPaymentStart();

    paypal.someMethod()
      .then(() => {
        onPaymentSucceed();
      })
      .catch(error => {
        onPaymentError(error.message);
      });
  };

  return (
    <form onSubmit={handlePayment}>
      <PayPalForm />
      <button type="submit">Pay</button>
    </form>
  );
};

Enter fullscreen mode Exit fullscreen mode

Now, you can create a simple container that mount the checkout component and pass the correct props.


import PayPalContainer from './paypal.container';

const CheckoutContainer = () => (
  <CheckoutComponent provider={PayPalContainer} // any props here />
);

Enter fullscreen mode Exit fullscreen mode

You can even create a factory method that returns the correct provider in a scenario you have more than one.


export enum Provider {
  PAYPAL = "PAYPAL",
  STRIPE = "STRIPE"
}

const getSelectedProvider = (): Provider => {...};

const getProviderContainer = (provider: Provider) => {
  switch (provider) {
    case Provider.PAYPAL:
      return PayPalContainer;
    case Provider.STRIPE:
      return StripeProvider;
  }
};

const CheckoutContainer = () => {
  const provider = getSelectedProvider();

  return <CheckoutComponent provider={getProviderContainer(provider)} />;
};

Enter fullscreen mode Exit fullscreen mode

I hope that this article helped you to understand how to use inversion of control with react and how to decouple your code to be more maintainable.

Build with independent components, for speed and scale
Instead of building monolithic apps, build independent components first and compose them into features and applications. It makes development faster and helps teams build more consistent and scalable applications.
OSS Tools like Bit offer a great developer experience for building independent components and composing applications. Many teams start by building their Design Systems or Micro Frontends, through independent components. Give it a try →

Image description

Top comments (0)