How to share information between frontend components in Akeneo PIM with mediator pattern?

As part of my previous blog posts, I introduced you to mediators as a pattern that enables communication between Akeneo components (mediation). Now I would like to show you how to use the mediator pattern and present you with good practices. At first, I will give a complete rundown on mediators in React components. Then I will switch to usage in RequireJS modules.

If you are not familiar with this pattern yet – don't worry. You can visit this page to learn the basics. This is a good place to start. Once you understand how to implement mediators and what the differences are between the mediators and observer pattern or facade pattern, you can take all of your skills to the next level with me.🧑‍🎓


My blog post series provides some context and explanation of the principles used to build UI in the PIM. Check the other articles:


Mediators in React components – Explained with examples

The primary role of mediators is sending information directly FROM one component/module where a trigger is defined TO another (one or more) where listeners are set.

To use a mediator, first of all you have to import it and assign to a variable in the following way:

const mediator = require('oro/mediator');

In case of a component which triggers a mediator, you should use the default trigger method. The method has two arguments. The first one is the name of the event which you can define as you want.

📢 Keep in mind that the name has to be the same for all React components listening for the event.

The second argument is a payload that will be sent between components. It could be a number, string, array or object you need.

In the example below, mediator is wrapped into prepareCreatedPackageToAssign function which takes id of created package as an argument:

prepareCreatedPackageToAssign = (packageId: string) => {
   mediator.trigger("pim_reference_entity:package_was_created", packageId);
};

For the second component in the constructor method, you have to add event listener using on method:

constructor(props: RecordSelectorProps & any) {
   super(props);
   this.DOMel = React.createRef();

   mediator.on('pim_reference_entity:package_was_created', (payload: string) => {
       this.addNewPackageToCurrentProduct(payload);
   });
}

The syntax is similar to the trigger function from the first component. There are also two arguments to define in the function: the first one is the name of the event and the second one is the anonymous function that will be executed. The function takes payload as an argument which is sent from the trigger function.

In the example React component in the constructor, there is a mediator declared which listens for the event pim_reference_entity: package_was_created and receives an argument. When the event takes place (and mediator will be notified about the event), the addNewPackageToCurrentProduct function will fire with the passed argument.

Alternative approach: useMediator hook

For newer functional components you can also use a custom hook – useMediator. You can find the source code of this hook here.

Here's an example of an import:

import {useMediator} from '@akeneo-pim-community/shared';
const mediator = useMediator();

In this approach, initialization of the React event listener should be added in the useEffect hook because the constructor method is available only for class-based components. In the example below, useMediator was used together with the useState hook. Depending on mediator type, value is set to the attributesLoaded variable:

import {useEffect, useLayoutEffect, useState} from 'react';
import {Product} from '../models';
import {useMediator} from '@akeneo-pim-community/shared';

const useScrollToAttribute = (product: Product) => {
   const [attributesLoaded, setAttributesLoaded] = useState(true);
   const [attributeToScrollTo, setAttributeToScrollTo] = useState<string | null>(null);
   const [isAttributeDisplayed, setIsAttributeDisplayed] = useState(false);
   const mediator = useMediator();

   useEffect(() => {
       const attributesLoadingHandler = () => setAttributesLoaded(false);
       mediator.on('ATTRIBUTES_LOADING', attributesLoadingHandler);
       const attributesLoadedHandler = () => setAttributesLoaded(true);
       mediator.on('ATTRIBUTES_LOADED', attributesLoadedHandler);
   }, []);

   // rest of the component
}

The whole code of the presented component is available on GitHub.

How to use mediator pattern in RequireJS modules?

Creating mediators in RequireJS modules is very similar to React components.

The biggest (and in most cases the only) difference is the way of defining listeners. In the example above where I introduced React class component, on method was added in the constructor method. But in case of RequireJS module, you usually add the listener in the initialize method:

initialize: function (config) {
   this.config = config.config;

   mediator.on('pim_reference_entity:package_was_created', (payload) => {
       this.addNewPackageToCurrentProduct(payload);
   });
  
   BaseForm.prototype.initialize.apply(this, arguments);
}

Good practices to follow

Now I would like to share with you some coding standards and good practices that can be applied in your project. Whether you’re creating your mediator in RequireJS modules or in React components, you can apply more advanced constructions.

1. In some cases, the mediator should be run only once. In this case you can use the once method:

mediator.once('pim_reference_entity:package_was_created
, (payload) => {
   // some action here
});

2. You can also remove listeners if some condition is met. If the payload is equal to 0, the listeners in this component will be removed:

mediator.on('pim_reference_entity:package_was_created', (payload: number) => {
   if (payload === 0) {
       mediator.off('pim_reference_entity:package_was_created');
   }

   // some action here
});

3. It's possible to combine the useEffect hook with the mediator off method. This hook will be executed only once.

💡 Good to know

If you add an empty array as a second argument, the hook will be executed only once.


4. In the cleanup callback, you can add a function that will remove the listener after meeting the condition:

useEffect(() => {
   const attributesLoadingHandler = () => setAttributesLoaded(false);
   mediator.on('ATTRIBUTES_LOADING', attributesLoadingHandler);
   const attributesLoadedHandler = () => setAttributesLoaded(true);
   mediator.on('ATTRIBUTES_LOADED', attributesLoadedHandler);

   if (sessionStorage.getItem('attributeToScrollTo')) {
       setAttributeToScrollTo(sessionStorage.getItem('attributeToScrollTo'));
       sessionStorage.removeItem('attributeToScrollTo');
   }

   return () => {
       mediator.off('ATTRIBUTES_LOADING', attributesLoadingHandler);
       mediator.off('ATTRIBUTES_LOADED', attributesLoadedHandler);
   };
}, []);

I hope this post helped you move on with how the objects communicate through the mediator in Akeneo PIM. Now it’s time to write your own code. ;)

If you have a question, feel free to message me on LinkedIn. 💬

Great ideas require great technology solutions