ScandiPWA components are reusable, extendable, and independent UI logic that divides the UI into smaller pieces to make them easier to manage and share across an application.
The ScandiPWA theme utilizes **React class-based components** defined in the component
directory, and all components may follow the same structure for consistency across the project and make it easier for different people to work together in different aspects of the project.
ScandiPWA operates following a complex architecture and has its own lifecycle. There is a set of rules in the internal structure that makes the application work and separates the raw templates from business logic, make sure you’re familiarized with it.
What is the architecture of ScandiPWA?
The most recommended way to create or install a new ScandiPWA component is via the official ScandiPWA CLI. Make sure to keep your code extensible and import declarations well-structured!
ScandiPWA utilizes class components, which makes it easier to override methods and properties when compared to function components. This makes it a perfect combination to work with extensions and plugins.
This is a component based on ES6 class that extends React.Component
or React.PureComponent
class:
<aside>
➡️ ScandiPWA uses React.PureComponent
over React.Component
. This is done to improve performance. React.PureComponent
utilizes simple props caching to avoid unnecessary re-renders.
</aside>
import { PureComponent } from 'react'
class Welcome extends PureComponent {
render() {
// this can access any props defined as name in the container
return <h1>Hello, {this.props.name}</h1>;
}
}
<aside> ➡️ Whether you declare a component, it must never modify its own props. Such functions are called “pure” because they do not attempt to change their inputs, and always return the same result for the same inputs. All React components must act like pure functions with respect to their props.
</aside>
Run the following command:
scandipwa create component [--container] [--redux] <name>
Where:
-container
/c
creates a container file for the component-redux
/r
connects the component to the Redux store with the connect
HOCname
is the name of the component to be created<aside> ➡️
When creating a component, the component name must start with an upper case letter.
</aside>
<aside> 🚨 The ScandiPWA CLI approach is the most indicated for component creation. Learn more about ScandiPWA CLI:
</aside>
To ensure consistency among components made by different people in a project, a defined structure of rules and directories should be established to make them work together.
<aside>
ℹ️ In ScandiPWA, components are located under src/component
for simple components, or src/route
for components that are used as the main components in routes. Component names follow the UpperCamelCase (also known as PascalCase) pattern. For example, ProductList
.
</aside>
This section explains how to structure each component file, describing the behaviour of each file, as well as their responsibilities. A component named <COMPONENT>
will be defined in the directory component/<COMPONENT>
containing the following files:
<COMPONENT>.component.js
- exports a class named <Component>
which is responsible for rendering the component to UI.
<COMPONENT>.container.js
(optional) - a class named <Component>Container
, which will be responsible for the implementation of the business logic of the component.
It may contain the usage of namespaces @namespaces
e.g.: /** @namespace Component/ProductPrice/Component */
Common patterns, that apply to all types of files:
Use of namespaces
Namespaces are important for allowing others to easily extend your application through the use of plugins. Therefore, always include namespaces in your components!
To add a namespace, include /** @namespace <COMPONENT NAMESPACE> */
before you declare your component.
/** @namespace Component/CartOverlay/Container/mapStateToProps */
export const mapStateToProps = (state) => ({
//...
});
/** @namespace Component/CartOverlay/Container */
export class CartOverlayContainer extends PureComponent {
//...
}
Default export and non-default export
You should always export the component's class and functions so that they can be used when extending the component. Only the default export will actually be used when rendering the component.
This becomes important if the default export is wrapped in a HOC such as withRouter
, making it impossible to extend as a class.
Here is an example of what the ProductPrice
component might look like:
/** @namespace Component/ProductPrice/Component */
export class ProductPrice extends PureComponent {
// [..]
}
export default ProductPrice;
Structure of import declarations
Imports allow you to reuse code from other parts of the application. But how do you properly structure them? Please use the following structure (an example below is coming from the ProductPrice
component):
// import order should be kept consistent
// library imports first
import PropTypes from 'prop-types';
// absolute imports from other directories are second
import TextPlaceholder from 'Component/TextPlaceholder';
import { PriceType } from 'Type/ProductList';
// relative imports from the same directory are last
import './ProductPrice.style';
// ^ the .component file is responsible for importing the stylesheet (.style file)
<aside>
➡️ There is no need to import React
and PureComponent
- it is automatically imported.
</aside>
Use of PropTypes
<aside>
➡️ This applies to both .component.js
and .container.js files
.
</aside>
PropTypes
is a library for React that verifies at runtime if props have the correct type, and if required props are passed.
import PropTypes from 'prop-types';
/** @namespace Component/CheckoutPayment/Component */
export class CheckoutPayment extends PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,
isSelected: PropTypes.bool
};
// ...
}
Learn more about type checking:
.component.js
files?This file is responsible only for the UI rendering implementation via JSX
. The .component
file shouldn't be responsible for any business logic, such as fetching or manipulating data. For better separation of concerns, move all business logic to the .container
file. (However, it is allowed to maintain basic UI states e.g. to keep track of whether an accordion is open)
<aside>
ℹ️ You are able to access props defined in the container through this.props.<PROPS_NAME>
, for example, this.props.age
, or by destructuring it like const { age } = this.props
.
</aside>
Structuring it properly is key to a well-written component. There are just two rules for structuring it:
ScandiPWA uses React.PureComponent
over React.Component
. This is done to improve performance. React.PureComponent
utilizes simple props caching to avoid unnecessary re-renders.
Here's a simplified and annotated version of component/ProductPrice/ProductPrice.component.js
:
component/ProductPrice/ProductPrice.component.js
<aside>
✅ Note that this component is broken down by defining one function for each part. This is better than writing one long render
method for several reasons:
Besides the use of namespaces, default export, non-default export, and import declarations structure, ScandiPWA components have these common patterns:
Render Maps
Occasionally, a component needs to render different things depending on its state. Additionally, this state might affect an aspect of the component that is common between some states. To understand, consider an example:
Checkout
component exampleThe Checkout
component has several steps to guide the user through checkout: shipping, billing, and the success step. Some of these have features in common - during both shipping and billing steps, the customer needs to see the cart items and totals, but we hide this at the success step. All 3 steps render a page title, but the title is different for each step. All 3 steps are associated with a URI, but it is different for each step.
Of course, we could handle these changes with several if
and switch
statements, but ScandiPWA offers a better approach: treating the possible steps as data, stored in a field named stepMap
:
<aside>
✅ By configuring the different steps in a JavaScript object, we avoid duplicating code everywhere that needs to switch functionality depending on the current step. We also make it easier for plugins and child themes to augment the default functionality by simply changing the stepMap
values.
In addition, we can treat the steps as data, and easily find what the next or previous step should be, without additional data.
</aside>
Similar map
objects are used throughout ScandiPWA, with similar uses.
.container.js
files?The container must implement logic. A logic must prepare the props (data) for a component for rendering (How to create a component?). The container might listen to a Redux store (How to listen to Redux stores?).
The container file is responsible for:
connect
to get global state from Redux).component
so that it needs to do as little work as possibleBesides the use of namespaces, default export, non-default export, and import declarations structure, ScandiPWA containers have these common patterns:
containerProps
Usually, a container needs to pass on certain values to its corresponding component. These are all passed on in the render
method, where the .component
is given all the props it needs. However, for a shorter render
method and better-organized code, it is a good practice to define a separate function for the values you want to pass (additionally, this is easier to extend in child themes and plugins). Then the render
method merely needs to call this function, which will return some props coming from the container, hence the name.
Footer
container examplecontainerFunctions
Similarly, if a container implements certain business logic for its component, it may wish to pass this implementation as a prop so that it can be called from the .component
. These functions need to be defined in the .container
, then you can bind
them to this
so that they have access to the instance of the container, which they will need if they access this.props
or this.state
.
mapDispatchToProps
mapDispatchToProps
accepts a dispatch function from Redux and enables the container to make certain (async) updates.
export const mapDispatchToProps = (dispatch) => ({
updateTotals: (options) => CartDispatcher.then(
({ default: dispatcher }) => dispatcher.updateTotals(dispatch, options)
),
showOverlay: (overlayKey) => dispatch(toggleOverlayByKey(overlayKey)),
showNotification: (type, message) => dispatch(showNotification(type, message)),
});
mapStateToProps
mapStateToProps
is a function that receives a global state
object from Redux and passes on some selected values from the state which will end up being received as the container's props. mapStateToProps
needs a @namespace
declaration as well for plugins to work.
export const mapStateToProps = (state) => ({
totals: state.CartReducer.cartTotals,
device: state.ConfigReducer.device,
currencyCode: state.CartReducer.cartTotals.quote_currency_code,
activeOverlay: state.OverlayReducer.activeOverlay
});
connect
connect
is a React-Redux HOC that takes (mapStateToProps
, mapDispatchToProps
) as defined below and enables them to receive values from the global state, and passes their return values as props
export default connect(mapStateToProps, mapDispatchToProps)(CartOverlayContainer);
For example:
component/CartOverlay/CartOverlay.container.js
:.config.js
files?Due to Webpack optimization limitations, it is better to practice and more efficient to define constants you use in .component
or .container
in a separate file and import them where you need. This is what .config
files are for.
You can export constants by defining them in .config
file:
export const DISPLAY_PRODUCT_PRICES_IN_CATALOG_INCL_TAX = 'DISPLAY_PRODUCT_PRICES_IN_CATALOG_INCL_TAX';
export const DISPLAY_PRODUCT_PRICES_IN_CATALOG_EXCL_TAX = 'DISPLAY_PRODUCT_PRICES_IN_CATALOG_EXCL_TAX';
export const DISPLAY_PRODUCT_PRICES_IN_CATALOG_BOTH = 'DISPLAY_PRODUCT_PRICES_IN_CATALOG_BOTH';
export const IMAGE_LOADING = 0;
export const IMAGE_LOADED = 1;
export const IMAGE_NOT_FOUND = 2;
export const IMAGE_NOT_SPECIFIED = 3;
<aside> ✅ How to use these values? to use these constants just import where you need it, for example:
import {
IMAGE_LOADING, IMAGE_LOADED, IMAGE_NOT_FOUND
} from 'component/Image/Image.config';
</aside>
.style.scss
files?