Learning ScandiPWA Way

In this tutorial we will talk about the main component files in ScandiPWA:

After watching this tutorial you should be able to discuss the following:

  • Using VSCode extension for component bootstrap

  • Top-level contents must be exported in .config file

  • Map property principle

  • Escaping for loops and lets

To follow along with this tutorial, you should start with the environment set-up. For this demonstration we’ll be using VSCode.

Run yarn start to start the development server and add an index.js file to the src folder:

📂<your-app-name>
 ┣ 📂i18n           
 ┣ 📂magento
 ┣ 📂node_modules   
 ┣ 📂public         
 ┣ 📂src
 ┃ ┗ 📜index.js     # new file            
 ┣ 📜composer.json
 ┣ 📜package.json
 ┗ 📜yarn.lock

The index.js file should look like this:

import ReactDOM from 'react-dom';

ReactDOM.render(
    <UrlResolver />,
    document.getElementById('root')
);

You’ll see an error saying ‘UrlResolver’ is not defined. If you’ve followed along with the environment set-up, you’ll already have ScandiPWA Development Toolkit installed, which will be necessary for the rest of this tutorial.

To resolve the ‘not defined’ issue, press ctrl + shift + P to access the VSCode command pallette and type in ‘>Create new component’ and press enter.

You should then be prompted to type in your new component’s name, in this case it’s UrlResolver, select the Contains business logic feature and press enter.

The ScandiPWA Development Toolkit will generate a ScandiPWA UrlResolver component folder which will contain template files:

📂<your-app-name>
 ┣ 📂i18n           
 ┣ 📂magento
 ┣ 📂node_modules   
 ┣ 📂public         
 ┣ 📂src
 ┃ ┣ 📂component/UrlResolver
 ┃ ┃ ┣ 📜index.js
 ┃ ┃ ┣ 📜UrlResolver.component.js
 ┃ ┃ ┣ 📜UrlResolver.container.js
 ┃ ┃ ┗ 📜UrlResolver.style.scss
 ┃ ┗ 📜index.js                 
 ┣ 📜composer.json
 ┣ 📜package.json
 ┗ 📜yarn.lock

If you have any issues with the imports, ScandiPWA Development Toolkit allows you to click on your issue and ‘Fix all auto-fixable problems’.

So, now we can go back to src/index.js and import the UrlResolver:

import ReactDOM from 'react-dom';

import UrlResolver from 'Component/UrlResolver';

ReactDOM.render(
    <UrlResolver />,
    document.getElementById('root')
);

Notice that we don’t have to use the relative path, instead we can use an alias for the absolute path of the Component folder.

You can check out what path aliases are available by going to node_modules/@scandipwa/scandipwa/src. The folders here represent the available aliases and these are as follows: component, query, route, store, style, type and util. The alias for referencing these folders is simply the folder name - capitalized.

.component

Using the previously created component/UrlResolver folder, let’s edit the UrlResolver.component.js file:

// some stuff

render() {
    return (
        <div block="UrlResolver">
            Hello!
        </div>    
    );
}

If you go to Chrome localhost:3000, you’ll see the ‘Hello!’ being output there.

NOTE

The main tasks of UrlResolver.component.js are:

  • Determining the page type

  • Rendering the proper page

So, we’ll need to take a URL using the location API and we’ll need to detect to which entity type the URL refers to, e.g. a page, a category or a product.

First, let’s look at page rendering. Let’s assume that there are multiple page types. How can we render them?

Since the component.js files are made for pure rendering, we’ll assume that the page type will be coming from props and we’ll not determine page type in the component.

The URL determination will be done in the container.js and we’ll tackle that a bit later.

Let’s assume that the container ships us a type as a prop:

import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import './UrlResolver.style';

class UrlResolver extends PureComponent {
    static propTypes = {
        // a propType of type string is required
        type: PropTypes.string.isRequired
    };

    render() {
        const { type } = this.props;

        if (type === 'product') {
            return 'product';
        } 
        
        if (type === 'category') {
            return 'category';
        }
        return 'cms_page';
    }
}

Let’s add individual render methods for each product type:

import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import './UrlResolver.style';

class UrlResolver extends PureComponent {
    static propTypes = {
        // a propType of type string is required
        type: PropTypes.string.isRequired
    };

    renderProduct() {
        return 'product';
    }

    renderCategory() {
        return 'category';
    }

    renderCmsPage() {
        return 'cms_page';
    }

    render404() {
        return '404';
    }

    render() {
        const { type } = this.props;

        if (type === PRODUCT_TYPE) {
            return this.renderProduct();
        } 
        
        if (type === CATEGORY_TYPE) {
            return this.renderCategory();
        }
        return this.renderCmsPage();
    }
    // some stuff
}

.config

Add the product page types as constants in UrlResolver.config.js:

export const PRODUCT_TYPE = 'product';
export const CATEGORY_TYPE = 'category';
export const CMS_PAGE_TYPE = 'cms_page';

We shouldn’t add the constants to the component.js file due to the fact that in order for webpack to be able to create smaller sized bundles, we need to add the constants that might be reused by different modules to the config.js file - outside of large modules.

webpack can’t split modules apart, so, if only a constant is needed, the bundle size would be minimized significally by using a constant-only file. In this case, creating a new file means creating a new module.

The issue with our UrlResolver.component.js file now is that the code repeats itself. An option is to write switch statements instead of if statements:

// some stuff

render(){
    const { type } = this.props;

    switch (type) {
    case PRODUCT_TYPE:
        return this.renderProduct();
    case CATEGORY_TYPE:
        return this.renderCategory(); 
    case CMS_PAGE_TYPE:
        return this.renderCmsPage(); 
    default:
        return this.render404();
    }
}

Notice that ScandiPWA has a specific writing convention in place for switch statements - the cases should be on the same indentation level as the switch itself.

Since we haven’t imported the constants from the config file, we can use auto-fixer to ‘Fix this simple import-sort/sort problem’ and it’ll add the following to our imports:

import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import {
    CATEGORY_TYPE,
    CMS_PAGE_TYPE,
    PRODUCT_TYPE
} from './UrlResolver.config';

import './UrlResolver.style';

If we check-in with the browser, localhost:3000 will display ‘404’ as the type of page hasn’t yet been passed, it’s undefined.

Map approach for component types

The switch approach for component types is still not the most efficient way to go about rendering since we can’t quickly extend the method. The solution to this is to create a rendering map:

class UrlResolver extends PureComponent {
    static propTypes = {
        type: PropTypes.string.isRequired
    };

    renderMap = {
        [CATEGORY_TYPE]: this.renderCategory.bind(this),
        [PRODUCT_TYPE]: this.renderProduct.bind(this),
        [CMS_PAGE_TYPE]: this.renderCmsPage.bind(this),
    };

    // other stuff

As you might know this can cause context loss, so we need to bind the type to the render method.

After creating the render map, we need to replace our switch statement:

// some stuff

render(){
    const { type } = this.props;

    const renderFunction = this.renderMap[type];

    if (renderFunction) {
        return renderFunction();
    }

    return this.render404();

}

We can further optimize this by using the logical operator OR (||)

// some stuff

render(){
    const { type } = this.props;

    const renderFunction = this.renderMap[type] || this.render404.bind(this);
    return renderFunction();
}

In both cases our render will return ‘404’ in the case if no type was found.

So, the finalized component file will look like this:

import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import {
    CATEGORY_TYPE,
    CMS_PAGE_TYPE,
    PRODUCT_TYPE
} from './UrlResolver.config';

import './UrlResolver.style';

class UrlResolver extends PureComponent {
    static propTypes = {
        type: PropTypes.string.isRequired
    };

    renderMap = {
        [CATEGORY_TYPE]: this.renderCategory.bind(this),
        [PRODUCT_TYPE]: this.renderProduct.bind(this),
        [CMS_PAGE_TYPE]: this.renderCmsPage.bind(this),
        // add a new [KEY] to object to extend
        // type constants are in config file 
    };

    renderProduct() {
        return 'product';
    }

    renderCategory() {
        return 'category';
    }

    renderCmsPage() {
        return 'cms_page';
    }

    render404() {
        return '404';
    }

    render(){
        const { type } = this.props;

        const renderFunction = this.renderMap[type] || this.render404.bind(this);
        return renderFunction();
    }
}

export default UrlResolver;

.container

The container has all of the business logic inside of it. So, in order to determine the URL, we should go to the URL container. We can try to guess the URL type based on the URL, but we can also request the URL from the Magento URL resolver aka UrlRewrite\resolve.

For now, let’s just guess which type we’re referring to based on the location. So, to actually do it we should first understand the concept of a container.

Container structure: containerProps, containerFunctions

A container file has two functions always defined:

containerFunctions = {
    // getData: this.getData.bind(this)
};

containerProps = () => {
    // isDisabled: this._getIsDisabled()
};

containerFunctions is an object containing the mapping of a key that will be later passed to your component as a prop and the function that’ll be used to implement the prop.

The getData key will be passed as a prop to the component, where it’ll call it and then it’ll return some data. So, the logic itself will be located in the container and the component will simply call it.

We won’t be using the containerFunctions in this tutorial, so we can remove all references to it from the container file.

Next are the containerProps which are meant for props mapping. For example, if you have a property to define, like isDisabled or a type, you provide a function that will get this property for you.

Again, the property itself is used in the component for rendering, but the value retrieved from the container.

In order to determine the page type let’s do the following in the container.js file.

// add () to get a valid object returned, instead of a function
containerProps = () => ({
    type: this._getTypeFromURL()
});

_getTypeFromURL() {
    return PRODUCT_TYPE;
}

// some stuff

In order to use PRODUCT_TYPE we also need to import it in the UrlResolver.container.js file:

import { PureComponent } from 'react';

import UrlResolver from './UrlResolver.component';
import { PRODUCT_TYPE } from './UrlResolver.config';

If we compile and go to localhost:3000 in our browser, we should see ‘product’ now.

Now, we need to implement mapping itself. Let’s assume that the page we’re visiting contains the type and the URL. This means that we need to implement mapping again. This time instead of mapping to a function, let’s use regex, since it’s one of the faster ways to find strings.

// some stuff

typeMap = {
    [CATEGORY_TYPE]: /category/,
    [PRODUCT_TYPE]: /product/,
    [CMS_PAGE_TYPE]: /page/,
};

containerProps = () => ({
    type: this._getTypeFromURL()
});

_getTypeFromURL() {
    // eslint-disable-next-line fp/no-let
    for (let i = 0; i < Object.entries.(this.typeMap).length; i++){
        const [type, regex] = Object.entries(this.typeMap)[i];

        // checking if the provided path name matches
        if (regex.test(window.location.pathname)) {
            return type;
        }
    }
    return ''; // will be handled as 404 by component
}

Disable any ESlint warnings and don’t forget to:

import {
    CATEGORY_TYPE,
    CMS_PAGE_TYPE,
    PRODUCT_TYPE
} from './UrlResolver.config';

So in localhost:3000 we’ll see ‘404’, but if we go to localhost:3000/product in our browser, we’ll see ‘product’.

A way of optimizing the for loop would be by defining the array beforehand:

_getTypeFromURL() {
    const array = Object.entries.(this.typeMap);

    // eslint-disable-next-line fp/no-let
    for (let i = 0; i < array.length; i++){
        const [type, regex] = array[i];

        // checking if the provided path name matches
        if (regex.test(window.location.pathname)) {
            return type;
        }
    }
    return ''; // will be handled as 404 by component
}

Instead of the type map, we should make a type list that’ll work as an array. This will work much better for us, as it’ll be possible to loop through it right away:

typeList = [
    {
        type: CATEGORY_TYPE,
        regex: /category/
    },
    {
        type: CMS_PAGE_TYPE,
        regex: /page/
    },
    {
        type: PRODUCT_TYPE,
        regex: /product/
    },
]

containerProps = () => ({
    type: this._getTypeFromURL()
});

_getTypeFromURL() {
    // eslint-disable-next-line fp/no-let
    for (let i = 0; i < this.typeList.length; i++){
        const { type, regex } = this.typeList[i];

        // checking if the provided path name matches
        if (regex.test(window.location.pathname)) {
            return type;
        }
    }
    return ''; // will be handled as 404 by component
}

Another way of optimizing would be by using array functions:

_getTypeFromURL() {
    const { type } = this.typeList.find(
        ({ type, regex }) => regex.test(window.location.pathname)
    );
    return type;
}

An issue with using find is that it can return null. In this case we’ll get a TypeError: Cannot destructure property ‘type’ because it’s undefined.

A solution would be to return an empty array in case nothing is found. If an empty array is destructured the type is ‘undefined’, this then can be handled by the component:

_getTypeFromURL() {
    const { type } = this.typeList.find(
        ({ type, regex }) => regex.test(window.location.pathname)
    ) || {};

    return type; // will be handled as 404 if undefined by component
}

We can see that by using typeList the rest of our logic shrunk down as well. This is why we need to understand which data structure is needed before attempting to implement anything.

For example, arrays come in handy when you need to find something, but for rendering maps are a better solution.

.style

Let’s go back to the UrlResolver.component.js file and implement renderType as a separate function. This is needed because the render itself should return the style wrapper:

// some stuff

    render404() {
        return '404';
    }

    renderType() {
        const { type = '404' } = this.props;
        const renderFunction = this.renderMap[type] || this.render404.bin(this);
        return (
            <article
                block="UrlResolver"
                elem="Type"
                mods={ { type } }
            >
                { renderFunction();}
            </article>
        );
    }

    render(){
        return (
            <main block="UrlResolver">
                { this.renderType() }
            </main>
        );
    }
}

export default UrlResolver;

Wrapping the renderFunction in article ensures that we’ll be able to reference to it using a BEM abstraction later on.

Go to UrlResolver.style.scss to apply some styles. If you want to take an in-depth look at ScandiPWA styling conventions, go here.

:root {
    --url-resolver-color: orange;
}

.UrlResolver {
    font-size: 20px;

    &-Type {
        color: var(--url-resolver-color);

        // type specific colors
        &_type {
            &_404 {
                color: red;
            }
        }
    }
}

The issue with &_404 { color: red; } is that we’re redefining a property, instead we should redefine the CSS custom variable:

:root {
    --url-resolver-color: orange;
}

.UrlResolver {
    font-size: 20px;

    &-Type {
        color: var(--url-resolver-color);

        // type specific colors
        &_type {
            &_404 {
                --url-resolver-color: red;
            }
        }
    }
}

What if we move the variable declaration a level up?

:root {
    --url-resolver-color: orange;
}

.UrlResolver {
    font-size: 20px;
    color: var(--url-resolver-color);

    &-Type {
        &_type {
            &_404 {
                --url-resolver-color: red;
            }
        }
    }
}

Now, the logic will stop working and ‘404’ will be orange. As mentioned in the previous tutorial the CSS custom variables are resolved from the top of the file.

The first element it’ll look at is the and there the variable will be defined in :root{}. Going further down to ,and further on we can see no declarations of this variable.

It’ll not care if the variable declaration appears inside the element, it only cares if the declaration happens above the element.

This is why color property declarations should appear on the same level or deeper than the variable re-declaration.

The only thing left to do is to get rid of the hardcoded red:

:root {
    --url-resolver-color: orange;
    --url-resolver-404-color: red;
}

.UrlResolver {
    font-size: 20px;
    color: var(--url-resolver-color);

    &-Type {
        &_type {
            &_404 {
                --url-resolver-color: var(--url-resolver-404-color);
            }
        }
    }
}

Last updated