21 ways to not break your ScandiPWA theme
In this article, we’ll get into the DOs and DONTs of Stylesheets, JavaScript, and a couple of general things. Please pay close attention and don’t forget to revisit this article whenever you’re working with any of these. It’s always cheaper in terms of resources and your nerves to do it in the right way straight from the start.
So, without further ado, here are 21 concrete ways to not break your ScandiPWA project and make sure it works the way it’s supposed to:
Stylesheets (SCSS)
1. Mixin nesting
When writing media queries, the `mobile`, `desktop` mixins are commonly used. The proper way to organize them is to write selectors outside mixin and keep only properties in.
For example:
.Button {
// Wrong
@include mobile {
&:hover {
text-decoration: none;
}
}// Correct
&:hover {
@include mobile {
text-decoration: none;
}
}
}
Why?
Because this allows to keep all the properties organized to a selector — you change the appearance of the selector on your media query (change it’s property), so keep only properties changing. Additionally, this allows for easy addition / removal of properties and other media mixins to the selector in the future.
2. Color organization
The CSS variable based coloring is advised on ScandiPWA based solutions. Why? Because it allows for easy color-scheme manipulation and brings an option of color dynamic adjustment. The colors for a component must be specified in `:root` element, the colors themselves, might be moved out into `variables` file.
For example:
// in variables.scss
$alpine-white: #fffee9;
$slate-grey: #708090;// in Component.style.scss
:root {
--component-background: #{$alpine-white};
}.Component {
background: var(--component-background);@include mobile {
--component-background: #{$slate-grey};
}
}
Why?
The colors should have a common name throughout the project. For easier reference, it is recommended to declare SCSS variables in the `variables` file. The used colors should be specific to a component, they may share the same color, but must be distinct in order to be easily adjusted in the future. This makes your styles more flexible.
3. Selector and function sorting
The complex styles commonly require multiple pseudo-classes, pseudo-elements, media queries, properties, selector-build-ups. It’s important to write them in the correct order to keep everything trackable.
For example:
.Component {
@include button;--button-padding: .5rem;color: var(--component-color);@include mobile {
--button-padding: 1rem;
}&:hover {
--component-color: #{$dark-grey};
}&::after {
content: '> ';
}&_active {
// Styles applied when element is active
}&-Image {
max-width: 20px;
}span {
font-size: 12px;
}
}
Why?
Sorting selectors, functions and properties is important for consistency and the correct work of stylesheets. Why? For example, the non-content based mixin has to be defined on top in order to be rewritten with properties later. The CSS variables must be declared before they are used, otherwise they won’t work. The pseudo-classes and pseudo-elements must be divided and not mixed together. And classes should go above, because those selectors are tightly related to elements.
4. Escaping selector nesting
The deeper the selector, the harder it is for a CSSOM to calculate and draw. The ideal selectors are all the same weight. In our case, we are using BEM, which allows for 010 consistent depth throughout the project.
For example:
// Source code
.Component {
&-Wrapper {
// Element properties&_wide {
// Modifier properties
}
}
}// Compiled code
.Component-Wrapper {
// Element properties
}.Component-Wrapper_wide {
// Modifier properties
}
Why?
As stated above, shallower selectors allow for quicker paints. This also allows for easy navigation within a file, and easier reading.
5. Value optimization
The values of CSS properties should never reference decimal values lower than a pixel. So, all values should be rounded to full pixels, or, if it’s impossible to round, the pixels themselves should be used. Also, values like 23px must be rounded to 5px, why? Because this allows for easy reading. The recommended stops are 5px for content-full elements and 1px for simple elements. Remember — 0 is redundant.
For example:
// Wrong
width: 23.5px;
max-width: 21px;
opacity: 0.415;
font-size: 1.23rem;
line-height: 1.456;
color: rgba(45, 234, 89, 0.456);// Correct
width: 25px;
max-width: 20px;
opacity: .4;
font-size: 1.25rem; // assuming 1rem = 12px
line-height: 1.5;
color: rgba(45, 234, 89, .45);
Why?
It is impossible to draw less than 1px (if it is not a clip path!). This means, if element height is not a rounded `px` value and it has one pixel border, it might not be rendered, as it’s location is in between the pixel grid of a website. Similarly the 2px border, might become 1px. The vertically aligned elements are often affected by non-rounded values. For example, the position of the second element might be offset. A lot of issues might be potentially caused by non-rounded values. To make sure this doesn’t happen, be sure to always code in accordance with the principles outlined above.
6. Use of rem / em / px
As mentioned in the previous section, the rounded values are very important. However, using `em` and `rem` can be very misleading — they are dynamic and oftentimes you don’t know what value it contains. To make it clear, `rem` is the body-font-size-based unit, while `em` is based on a parent element font-size. There are only two rules — make sure you are using values that can be rounded to a full pixel and you understand why you are using `rem` or `em` — most commonly for text distribution.
For example:
body {
font-size: 12px;
}// Wrong
padding: 1.3rem;
left: 2.35rem;// Correct
padding: 15px;
left: 28px;
padding-left: 1rem;
Why?
As stated previously, it’s due to rendering issues. But, any use of specific unit should be logically explainable. The `em` or `rem` mean that the use case must be somehow related to font-size. So, use them in places related to text.
7. Important rule escaping
Important is the most powerful form of CSS selector strength rules, it bypasses id, class, name, everything. It is very commonly used to override inline-styled elements. We must escape it to have our code supportable.
For example:
// <div class="third-party" style="font-size:12px;" />// Ok, if everyhting else is impossible
.third-party {
font-size: 12px !important;
}// However, there should be no declarations of !important
// in custom defined elements / stylesheets.
Why?
Because it’s impossible to override — it makes styles inconsistent. It’s much better to use third parties, that escape direct styling, by, for example, using custom stylesheets and applying CSS custom variables.
8. No fixed heights / width
When styling, it’s easy to define a height once, and keep it consistent, however, the assumption that content will always be the same and designs won’t change is weak. The recommended way to approach styling is by using `box-sizing: border-box` along with padding to define the element’s height. In order to achieve the effective behaviour of the element when loading use `min-height`.
For example:
// Wrong
.ProductName {
height: 20px;
margin: 1rem;
}// Correct
.ProductName {
min-height: 20px;
margin: 1rem;
}
Why?
Because this prevents from applying custom style to the element, breaks `flex` if element is involved in it, and overall complicates styling.
9. Stop specifying default property values
When working with apps such as Zeplin, developers tend to copy the whole stylesheet, instead of retrieving only the specific values. Then, the `font-style: normal;` or `font-weight: normal;` appears in code. Those values are mostly the defaults (which might be disabled in case of Zeplin). Make sure you are not supplying them!
For example:
// Wrong
font-size: 15px;
font-weight: 300;
font-style: normal;
font-stretch: normal;// Correct
font-size: 15px;
font-weight: 300;
Why?
Because they’re redundant and just make stylesheets more complex.
General
1. Not visual loading
When loading data to be displayed, we must display placeholders if this data will appear on a page. Why? Because then, from placeholders we quickly jump into the loaded state, without breaking the layout, making the transition from loading to loaded seamless. Please do not use conditional rendering to showcase this, implement it using early returns and test, test, test!
For example:
class ProductPrice extends Component {
render() {
const { price } = this.props;
if (!price) return null; // Bad
if (!price) return this.renderPlaceholder(); // OK/**
* Perfect, if your function can render same structure,
* but with palceholders if data is not present
*/return (
...
);
}propTypes = {
price: PropTypes.shape([ ... ]).isRequired
}
}
Why?
In the perfect situation, if you have not yet received data to be rendered, you must already render the full element structure, but with placeholders inside. This way, after data is received, there are no complex re-renders, just places were placeholders were. This eliminates page blinking.
2. Reduce your data to useful format
The data we are working with is often not in the format we expect it to be. It is commonly array, where we need order object, it is often organized by wrong property, etc. Make sure you are preparing your data well in your reducer or container.
For example:
/**
* Imagine you want stores arranged by city it is currenlty located
* But the data comes flat
*/// Doing this in your component is bad idea
// Reducer
const { stores } = action;
return { stores, ...state };// Component
const { stores } = this.props;
const storesByCity = stores.reduce((cityStores, store) => {
const { city } = store;
if (!cityStores[city]) cityStores[city] = [];
cityStores[city].push(store);
return cityStores;
}, {});// However, doing this in reducer is great!
const { storesData } = action;return {
stores: storesData.reduce((cityStores, store) => {
const { city } = store;
if (!cityStores[city]) cityStores[city] = [];
cityStores[city].push(store);
return cityStores;
}, {}),
...state
};
Why?
Because data is often used in multiple places, so preparing it once, instead of looping through each time, is essential to performance.
JavaScript
1. Make it a habit not to reassign variables
In a paradigm of functional programming, you should never reassign values to variables. The functions must follow the Maths way — if a value is passed, the value should be returned. The passed value should never be changed or redefined.
For example:
// Bad
let hairif (today === 'Monday') {
hair = 'bangs'
} else {
hair = 'something else'
}// Good
const hair = today === 'Monday' ? 'bangs' : 'something else';
Why?
Because you may change the external state by accident when you reassign value and you are creating a more complex code by doing this. There’s a great article regarding this that we recommend you to check out: Don’t Reassign!
2. Destructurize data
The dot notation is great, but when used a lot, it becomes hard to maintain. It’s much better practice to separate objects into smaller parts — destructurizing them into variables.
For example:
// Bad
return this.props.product.minPrice < this.props.product.maxPrice
? this.props.product.minPrice
: this.props.product.maxPrice;// Still bad
const minPrice = this.props.product.minPrice;
const maxprice = this.props.product.maxPrice;
return minPrice < maxPrice ? minPrice : maxPrice;// Great
const { product: { minPrice = 0, maxPrice = 0 } } = this.props
return minPrice < maxPrice ? minPrice : maxPrice;
Why?
This allows for easy reading of what is going on in the code. It also allows for easy default value declaration, without modifying the initial data source. What’s more, it allows for variable name aliasing.
3. Don’t overuse &&
It is known for a long time, that `&&` in javascript returns a last element if expression is true. But, the use of this behaviour leads to complex code structures which are hard to support.
For example:
// Bad, hard to read
return (
<div>
{ products && products.length && (
<h2>{ products[0].name }</h2>
) }
</div>
);// Perfect, if moved into separate function
if (!products.length) return null;
const [{ name }] = products;
return <h2>{ name }</h2>;
Why?
Because it is much better to delegate an addition function to handle your checks, rather to rely on weird mechanics. Also, escaping `&&` allows for cleaner code, and more extendable functions.
4. Avoid dynamic function creation
Using arrow functions is great, but they must be created in runtime. We must escape this behaviour by binding them to component context. There is also an option to declare them as arrow functions initially, but it is not recommended, as we would like to stay consistent throughout the project (in terms of function declarations).
For example:
class MyComponent extends Component {
constructor() {
this.handleOneMoreClick = this.handleOneMoreClick.bind(this);
}// Preferred way
handleOneMoreClick() { ... }// OK one, but not recommened
handleOtherClick: () => { ... }// Bad one
handleClick() { ... }render() {
return (
<>
{ /** Not the right way to go */ }
<button onClick={ () => this.handleClick } />
<button onClick={ this.handleOneMoreClick } />
<button onClick={ this.handleOtherClick } />
</>
);
}
}
Why?
Because we would like not avoid sacrificing any time while rendering. Slow renders are very destructive to an application, and lead to bad user experience.
5. Utilize more effective array functions
If you read through 1., you are familiar with escaping `var` and `let`. But, if working with an object or array you may use const, while still re-declaring values. Avoid this — think about it in the same way as 1.
Remember, the `forEach` is just a loop-through, the `map`, `reduce`, `filter`, `find` (and others!) exist and are more suitable for common purposes.
For example:
// Bad practise
const { stores } = this.props;
const cityStores = {};if (stores.length) {
stores.forEach((store) => {
const { city } = store;
if (!cityStores[city]) cityStores[city] = [];
cityStores[city].push(store);
});
}return cityStores;// Great approach
const { stores } = this.props;
return stores.reduce((cityStores, store) => {
const { city } = store;
if (!cityStores[city]) cityStores[city] = [];
cityStores[city].push(store);
return cityStores;
}, {});
Why?
Because as described in 1., the property re-declarations are ineffective. Use of suitable functions allows for less loops, and code in general.
6. Return immediately
Why bother with additional checks if we can return immediately?
For example:
// Bad
function doSomething(argument) {
if (argument) {
// many lines of code
return something;
}return false
}// Good
function doSomething(argument) {
if (!argument) {
return false;
}
// many lines of codereturn something;
}
Why?
Because this is cleaner and makes code much more readable and flat. Any complex structures make code less and less readable.
7. Remember, that variables may have any name
If you have a place where one variable name will be convenient, and your variable is coming from a function parameter, do not hesitate and immediately name it understandably. It is strange to see abstract variable names like `value`, which do not represent anything.
For example:
// Unpreferable, rename occurs
<input onChange={ value => this.setState({ name: value }) } /> // value// Prefered, rename does not happen
<input onChange={ name => this.setState({ name }) } /> // name// Prefered, complex item, rename occurs
<input onChange={ name => this.setState({ item: { name: itemName }, user: { name: userName } }) } /> // itemName, userName
Why?
First-of-all, it makes sense, because we are directly naming an entity we are working with. Secondly, we are writing less code.
8. No multiple state-updates at once
When implementing your own `dispatcher` classes you might see places where you stumble across a place where multiple `dispatch` functions are called. We must join them into one action dispatch.
For example:
// Inefficent
dispatch(updateProducts(products));
dispatch(updateBreadcrumbs(products));
dispatch(updateCategoryFilters(products));// Great
dispatch(updateProductsAndCategory(products);
Why?
Because, if not merged together, it will be hard to track from listening component. On global state update, the component will update multiple times, so tracking previous props in `static getDerivedStateFromProps` will not be easily achievable. Also, the rendering might happen more than one time => some lag might be noticable.
9. No complex conditional rendering
When implementing a react render method, the JSX is typed, it supports a conditional rendering by writing in `{ … }`. This feature is commonly over-used by developers, which results in very clunky, big render methods.
For example:
// Bad, very complex rendering
render() {
const { isOpen } = this.state;
const { product } = this.props;
const { name, attributes } = product;return (
<div>
{ name && (<h2>{ name }</h2>) }
<p>Attributes:</p>
<ul>
{ attributes && attributes.length && attributes.map(({ value, code }) => (
<li>
<strong>{ code }</strong>
{ value }
</li>
) }
</ul>
{ isOpen && <AnotherComponent product={ product } /> }
</div>
);
}// Great, clean render
renderAttributeList() {
const { product: { attributes = [] } } = this.props;
if (!attributes.length) return null;
return attributes.map(this.renderAttribute)
}renderAttribute({ value, code }) {
return (
<li>
<strong>{ code }</strong>
{ value }
</li>
);
}renderProductName() {
const { product: { name } } = this.props;
return <h2>{ name }</h2>;
}renderAnotherComponent() {
const { isOpen } = this.state;
const { product } = this.props;
if (!isOpen) return null;return <AnotherComponent product={ product } />;
}render() {
return (
<div>
{ this.renderProductName() }
<p>Attributes:</p>
<ul>
{ this.renderAttributeList() }
</ul>
{ this.renderAnotherComponent() }
</div>
);
}
Why?
Because dividing complex conditional renders in functions, makes it:
- More extendable
- More readable
- Allows for easy additional checks
The recommendation here is not to go and rewrite everything to functions, but to keep it clean. Complex render methods look dirty.
10. Don’t place redundant JSX tags
Coding and supporting the project is not easy, and sometimes unnecessary tags are left over. Make sure you remove them to keep clean code that’s easy to read.
For example:
// Bad
return (
<>
// <Item item={item} />
<div>
{ /** ... */ }
</div>
</>
);// Great
return (
<div>
{ /** ... */ }
</div>
);
Why?
Simple — because those tags are unnecessary.
Conclusion
The points outlined above aren’t just recommendations — it’s crucial to keep them in mind so you don’t break your ScandiPWA theme and make clean and accessible for yourself and others. If you’re looking for more information, make sure to explore the ScandiPWA documentation or just ask us in our #Slack channel and the community will be happy to help you!
Let us help you!
Want to improve your store’s performance, boost engagement, and increase conversions? Interested how open-source PWA theme for Magento can help you to stay ahead of your competition by leveraging the best cutting-edge technology for eCommerce right now?
Drop us a line at hello@scandipwa.com for a free PWA Demo!
ScandiPWA is the first open-source PWA theme for Magento.
Follow our social media accounts to be the first to know about any news and updates: