Simplifying Targetables in PWA Studio
Tags: magento, pwastudio, react
Published: 2021-03-31 19:11:39

After the positive feedback on my first [Simplifying Styling in PWA Studio](https://dev.to/chrisbrabender/simplifying-styling-in-pwa-studio-1ki1) article (thanks to everyone who gave me feedback there), I have put together the next iteration of the concept which focuses on **Simplifying Targetables in PWA Studio**.

I'm not going to go into the details on how to use Targetables, that has been covered in a few places which you can refer to:

* https://marcin-kwiatkowski.com/how-to-extend-pwa-studio-with-new-features (thanks for the shout out Marcin!)
* https://magento.github.io/pwa-studio/pwa-buildpack/reference/targetables/TargetableModule/

What I am going to cover here is the problem with the local-intercept.js file.

##What problem?
In our scaffolded PWA Studio project we have a single local-intercept.js file which is our starting point for working with Targets and Targetables. 

Having a single file to do *all* of our work here will quickly make this file unruly and frankly, just enormous. This makes it difficult to identify customisations (for debugging, finding what has been done etc) and ensure we aren't trying to intercept the same component twice. 

##What is the fix?
Following a similar concept to the Simpliying Styling process, I am proposing a file naming convention and structure to automatically detect and load relevant intercept files for specific modules.

So what does the structure look like? Let's assume we want to intercept the Header component
* `@magento/venia-ui/lib/components/Header/header.js`
* We could create a new file in our local project like this -  `src/components/Header/header.targetables.js`

Again if we wanted to intercept the ProductFullDetail component * `@magento/venia-ui/lib/components/ProductFullDetail/productFullDetail.js`)
* We could create a `src/components/ProductFullDeatil/productFullDetail.targetables.js` file

This way we can more easily identify, categorise and isolate our intercepts. Making it much easier to customise the project.

So assuming we have these files in place, how do we load them? And how do we use them?

##Updating local-intercept.js
We need to do a few things in our local-intercept.js file to identify and load these files. So we are going to use [globby](https://www.npmjs.com/package/globby) again to find our targetables files.

We are also going to use `babel-plugin-require-context-hook/register` to load the files inline and execute a function within those files (this means we can pass the component to our targetables files and run a standardised function to simplify things even more).

```javascript
  // Access our Targetables object
const { Targetables } = require('@magento/pwa-buildpack');
const targetables = Targetables.using(targets);

// Define the path to @magento packages
const magentoPath = 'node_modules/@magento';

const globby = require('globby');
const path = require('path');

// Context loader allows us to execute functions in the targeted file
const requireContextLoader = require('babel-plugin-require-context-hook/register')();

// Find our .targetables.js files
(async () => {
    const paths = await globby('src/components', {
        expandDirectories: {
        files: ['*.targetables.js']
        }
    });

    paths.forEach((myPath) => {
        const relativePath = myPath.replace('.targetables', '').replace(`src/components`, `${magentoPath}/venia-ui/lib/components`);
        const absolutePath = path.resolve(relativePath);

        fs.stat(absolutePath, (err, stat) => {
        if (!err && stat && stat.isFile()) {
            // Retrieve the react component from our cache (so we can use it more than once if necessary)
            const component = getReactComponent(relativePath.replace('node_modules/', ''));

            /** 
             * Load the targetables file for the component and execute the interceptComponent function
             * We also pass in the component itself so we don't need to load it in the file
            */
            const componentInterceptor = require('./' + myPath);
            componentInterceptor.interceptComponent(component);
        }
        });
    });
})();


// Create a cache of components so our styling and intercepts can use the same object
let componentsCache = [];
function getReactComponent(modulePath) {
    if (componentsCache[modulePath] !== undefined) {
        return componentsCache[modulePath];
    }

    return componentsCache[modulePath] = targetables.reactComponent(modulePath);
}
```

And that is all we need in our local-intercept.js file in order to load and execute all our *.targetables.js files! We don't need to touch local-intercept.js ever again to execute targetables functionality.

##Our *.targetables.js files
Within our *targetables.js files all we need to do is define our interceptComponent function and export it.

EG: `src/components/Header/header.targetables.js`

Here we are passing in the header.js component from Venia UI with the targetables functions ready to go. So we can simply do component.insertAfterJSX etc in our files.

```javascript
const interceptComponent = (component) => {
    component.addImport('import MegaMenu from "../../../../../../src/components/MegaMenu"');
    component.insertAfterJSX('Link', '<MegaMenu />');
    component.setJSXProps('Link', {
        'className':'{classes.logoContainer}'
    });
}

exports.interceptComponent = interceptComponent;
```

And this can be repeated to intercept any other Venia UI component we like:

EG 2: `src/components/Navigation/navigation.targetables.js`

Here we are just removing the h2 element from the navigation.

```javascript
const interceptComponent = (component) => {
    // Execute our JSX manipulation
    component.removeJSX('h2');
}

exports.interceptComponent = interceptComponent;
```

It's a pretty straight forward setup and I hope it will come in handy to help you manage your customisations for your new PWA Studio projects!

Please share and let me know if you have any feedback and/or if you implement this strategy!