bubble-icon

Plugins React

GraphCommerce's React plugin system allows you to extend GraphCommerce in a plug-and-play manner. Install a new package and the code will be added at the right places.

  • Plug-and-play: It is be possible to install packages after which they immediately work.
  • No runtime overhead: The plugin system is fully implemented in webpack and
  • Easy plugin creation: Configuration should happen in the plugin file, not a separate configuration file.

What problem are we solving?

Without plugins the only way to add new functionality is by modifying the code of your project at multiple places. We often pass props to components to customize them, but sometimes we also place hooks at multiple places.

For example, to add a new payment method it was necessary to modify the props of <PaymentMethodContextProvider methods={[...braintree]}>

This causes problems:

  • Upgrades: If GraphCommerce changes something somewhere in the code where you have already modified the code, you get an upgrade conflict which you have to manually resolve. By using plugins you can avoid this.
  • New packages: When you install a complex new package, it can happen that you have to modify the code of your project at multiple places. This is not necessary with plugins.

What is a plugin?

A plugin is a way to modify React Components by wrapping them, without having to modify the code in examples/magento-graphcms or your-project.

For the M2 people: Think of around plugins, but without configuration files and no performance penalty.

In the PR I have made the <PaymentMethodContextProvider /> work with plugins.

The actual plugins are:

The result of this is that:

  • The payment methods are added to the <PaymentMethodContextProvider /> via plugins.
  • These plugins are only applied if the relevant package is installed.

How do I make a plugin?

In the root of my project, i've created a plugin examples/magento-graphcms/plugins/AddPaymentMethodEnhancer.tsx that adds purchaseorder to <PaymentMethodContextProvider/>

import type { PaymentMethodContextProviderProps } from '@graphcommerce/magento-cart-payment-method'
import type { PluginProps } from '@graphcommerce/next-config'
import { purchaseorder } from '../PurchaseOrder'

// Component to extend, required
export const component = 'PaymentMethodContextProvider'

// Exported location of the component that you are extending, required
export const exported = '@graphcommerce/magento-cart-payment-method'

function AddPaymentMethodEnhancer(
  props: PluginProps<PaymentMethodContextProviderProps>,
) {
  const { Prev, modules, ...rest } = props
  return <Prev {...rest} modules={{ ...modules, purchaseorder }} />
}

/** The export must be named `Plugin` and must accept a Prev component to render */
export const Plugin = AddIncludedMethods

How does it work?

  1. It generates a list of all packages with graphcommerce in the package name (All @graphcommerce/* packages and @my-company/graphcommerce-plugin-name).
  2. It does a glob search for plugins in the plugins folders for each package: ${packageLocation}/plugins/**/*.tsx.
  3. Statically Analyse the plugins, check if the component and exported exports exist and generate the plugin configuration.
  4. Generate PaymentMethodContext.interceptor.tsx and place it next to the existing component

Example of generated interceptor with additional comments:

/* This file is automatically generated for @graphcommerce/magento-cart-payment-method */

export * from '.'
import { Plugin as GaPaymentMethodButton } from '@graphcommerce/googleanalytics/plugins/GaPaymentMethodButton'
import { Plugin as GaPaymentMethodContextProvider } from '@graphcommerce/googleanalytics/plugins/GaPaymentMethodContextProvider'
import { Plugin as AddIncludedMethods } from '@graphcommerce/magento-payment-included/plugins/AddIncludedMethods'
import { ComponentProps } from 'react'
import {
  PaymentMethodButton as PaymentMethodButtonBase,
  PaymentMethodContextProvider as PaymentMethodContextProviderBase,
} from '.'

/**
 * Interceptor for `<PaymentMethodButton/>` with these plugins:
 *
 * - `@graphcommerce/googleanalytics/plugins/GaPaymentMethodButton`
 */
type PaymentMethodButtonProps = ComponentProps<typeof PaymentMethodButtonBase>

function GaPaymentMethodButtonInterceptor(props: PaymentMethodButtonProps) {
  return <GaPaymentMethodButton {...props} Prev={PaymentMethodButtonBase} />
}
export const PaymentMethodButton = GaPaymentMethodButtonInterceptor

/**
 * Interceptor for `<PaymentMethodContextProvider/>` with these plugins:
 *
 * - `@graphcommerce/magento-payment-included/plugins/AddIncludedMethods`
 * - `@graphcommerce/googleanalytics/plugins/GaPaymentMethodContextProvider`
 */
type PaymentMethodContextProviderProps = ComponentProps<
  typeof PaymentMethodContextProviderBase
>

function AddIncludedMethodsInterceptor(
  props: PaymentMethodContextProviderProps,
) {
  return (
    <AddIncludedMethods {...props} Prev={PaymentMethodContextProviderBase} />
  )
}
function GaPaymentMethodContextProviderInterceptor(
  props: PaymentMethodContextProviderProps,
) {
  return (
    <GaPaymentMethodContextProvider
      {...props}
      Prev={AddIncludedMethodsInterceptor}
    />
  )
}
export const PaymentMethodContextProvider =
  GaPaymentMethodContextProviderInterceptor

React profile will show that all components are nested:

Possible use cases

In the examples above we've extended the payment methods, but it should also work for other things such as:

  • Googletagmanager
  • Googleanalytics
  • Google recaptcha
  • Compare functionality?
  • Wishlist functionality?
  • Abstraction between GraphCommerce and Backends? (Magento, BigCommerce, CommerceTools, etc.)

Conditionally include a plugin

Provide an ifEnv export in the plugin that will only include the plugin if the environment variable is set.

export const ifEnv = 'MY_ENV_VARIABLE'

When to use a plugin?

A plugin should be used when a new package is created that influences the behavior of other packages.

Plugin loading order

A plugin is injected later than the dependencies of the package. So if a plugin is loaded to early, make sure the package has a dependency on the other package.