Coding Principles
Below are rules behind an effective way of customizing the account
Data
In most cases, customizations will depend on data that is returned from Findify Search API
In order to check the returned data from the Search API response, you can use the Browsers dev-tools:
- Open devtools (press f12)
- Open
Network
tab - In search box type:
api-v3.findify.io
After that, you'll be able to check what data returned from API.
Data structures
Data immutability
. In Findify we are trying to use functional paradigm when coding. One of the main concepts of the functional approach is an immutable data structures (immutable lists and maps in most cases). For that matter we use immutablejs (https://github.com/immutable-js/immutable-js). So almost all data in Findify's MJS are entities of immutabljs Map or List classes.
Why do we use it? - To prevent unexpected data mutations!
For example:
// regular approach
const obj = { list: ['a'] }
// some code that will clean up the obj list
if (someBool) {
obj.list = []
}
console.log(obj.list[0].length) // throws an Error because the first item in the list is undefined
// immutable approach
import { Map, List } from 'immutable'
const map = new Map({ list: new List(['a']) })
// some code that will clean up the map list
if (someBool) {
map.set('list', new List([])) // set to return new instance, so the map list will remain the same
}
console.log(map.getIn(['list', 0]).length) // this will not throw an Error
Best Practises
-
PREVENT CAN'T READ PROPERTY OF UNDEFINED ERRORS
.ALWAYS
check if data that you trying to process is defined, e.g:tags
,custom_fields
to avoid errors likecan't read property of undefined
For example:
// check if the item tags contain the string "new"
// dangerous approach
const isNew = item.get('tags').find(t => t.toLowerCase() === 'new')
// if item doesn't have the "tags" property - it will throw an Error because we try to
// call function find of undefined
// safe approach
const tags = item.get('tags')
const isNew = tags
? tags.find(t => t.toLowerCase() === 'new')
: false
// use default values when initializing variables
const tags = item.get('tags', []) // in this case if the "tags" property is not in the item object - it will return an empty array
const isNew = tags.find(t => t.toLowerCase() === 'new')
ALWAYS USE LOWERCASED STRINGS WHILE DATA PROCESSING AND COMPARISONS
Do not use the string comparators without lowercasing each and every string, i.e. usestr1.toLowerCase() === str2.toLowerCase()
.
For example:
// check if the item has the "sale" tag, but the actual tag value might be "sale"/"SALE"/"Sale" ...
const isSaleItem = item.get('tags', []).find(t => t.toLowerCase() === 'sale');
-
MEMOIZE ALL COMPUTATIONS
- avoid iterating through data on each component render. For this you can use HOCwithPropsOnChange
from recompose (https://github.com/acdlite/recompose/blob/master/docs/API.md#withpropsonchange) or useMemo from React (https://ru.reactjs.org/docs/hooks-reference.html#usememo)For example:
// check that item tags contain the string "super"
// not optimized solution - will compute the prop on each component re-render
const hasSuper = item.get('tags', []).find(t => t.toLowerCase() === 'super');
//optimized solution - will compute the prop only when the item has changed
const hasSuper = useMemo( () => item.get('tags', []).find(t => t.toLowerCase() === 'super'), []);
-
DON'T WRITE COMPUTATION LOGIC DIRECTLY IN JSX
Always move the logic/conditions into separate Props. It will simplify code readability and improve JS code performance - this will result in less amount of bugs in production environment.For example:
// display sticker if tags contain string "new"
// bad approach
/**
* @module components/Cards/Product
*/
...
// bad approach
const ProductCardView = ({
item,
config,
theme,
}: any) => {
return (
<a>
...
<Sticker display-if={item.get('tags', []).find(t => t.toLowerCase() === 'new')}/>
...
</a>
)
}
// good approach
import { useMemo } from React;
const ProductCardView = ({
item,
config,
theme
}: any) => {
const displaySticker = useMemo(() => item.get('tags', []).find(t => t.toLowerCase() === 'new'), [item])
return (
<a>
...
<Sticker display-if={displaySticker}/>
...
</a>
)
}
-
DON'T USE HUGE TERNARY OPERATORS
If the logic is not a one-liner, do not use HUGE ternary operators, use small functions that will return the data you need. It will simplify code that will lead to an easier debugging.For example:
// instead of
const ProductCardView = ({
item,
config,
theme
}: any) => {
const booleanFlag = item.get('tags').includes('tag a') ? true : item.get('tags').includes('tag b') ? true : false;
return (
<a>
...
</a>
)
}
// create a separate function that will do the hard job
const getBooleanFlagByTags = (tags) => {
if (tags.includes('tag a')) {
return true;
}
if (tags.includes('tag b')) {
return true;
}
return false
}
const ProductCardView = ({
item,
config,
theme
}: any) => {
const booleanFlag = getBooleanFlagByTags(item.get('tags', []));
return (
<a>
...
</a>
)
}
- **`DON'T USE THIRDPARTY LIBRARIES TO MANIPULATE DOM`** Do not use jQuery or any other third party libraries, unless absolutely needed for some custom integrations - use regular DOM API. We try to initialize Findify JS as fast as it is possible and introducing 3d parties (e.g. jQuery or other libraries) might harm Findify performance as we will need to wait until they are initialized.
For example:
// update page title with query
// instead of JQuery:
$('title').text = query;
// use DOM:
const title = document.querySelector('title');
if (title) {
title.innerText = query;
}
-
ALWAYS CHECK THAT DOM/BOM API THAT YOU USE IS AVAILABLE
Before using some DOM API - check that it's available in all browsers that you want to support.For example:
// prepend element
// dangerous code
const containerElement = document.querySelector('.container')
const elementToPrepend = document.querySelector('.to-prepend')
containerElement.prepend(elementToPrepend)
// safe code
const containerElement = document.querySelector('.container')
const elementToPrepend = document.querySelector('.to-prepend')
if (typeof containerElement.prepend === 'function') {
containerElement.prepend(elementToPrepend)
} else {
// you decide what to use as a polyfil
prependPolyfil(containerElement, elemeÑŒntToPrepend)
}
Advanced
-
USE FACTORY COMPONENTS
If you have multiple different stickers/other components, please try to come up with one component that aggregates all different variations into one. So basically we need to implement "Factory" pattern and delegate responsibility of displaying correct Sticker to the other component. This is to simplify code readability that will lead to a less amount of bugs on production, it will be easier to refactor/fix existing features/solutions.For example:
// we should display:
// SUPER sticker - if item tags contain string "super"
// SALE sticker - if item tags contain string "sale"
// NEW sticker - if item tags contain string "new"
// bad approach
const ProductCardView = ({
item,
config,
theme
}: any) => {
const {
displaySuper,
displaySale,
displayNew
} = useMemo(() => {
const tags = item.get('tags', []);
if (tags.find(t => t.toLowerCase() === 'super')) {
return { displaySuper: true }
}
if (tags.find(t => t.toLowerCase() === 'sale')) {
return { displaySuper: true }
}
if (tags.find(t => t.toLowerCase() === 'new')) {
return { displaySuper: true }
}
}, [item])
return (
<a>
...
<SuperSticker display-if={displaySuper}/>
<SaleSticker display-if={displaySale}/>
<NewSticker display-if={displayNew}/>
...
</a>
)
}
// good approach
const Sticker = ({item}) => {
return (
<span className="thumb-badge">{item}</span>
)
}
const StickerSecond = ({item}) => {
return (
<span className="thumb-badge-second">{item}</span>
)
}
const getComponent = (sticker) =>
({
'New Product': Sticker,
'Clearance': Sticker,
'Bulk Buys': StickerSecond,
'Sale': StickerSecond,
'100% Markup': StickerSecond,
}[sticker] || (() => null));
const Stickers = memo((props: any) =>
createElement(
getComponent(props.item),
props
)
);
const CustomSticker = ({item}) => {
return (
<div display-if={item.getIn(['custom_fields', 'sticker'])} className="findify-product-badge">
<MapArray
array={item.getIn(['custom_fields', 'sticker'])}
factory={Stickers}
keyAccessor={(v, i) => i}
/>
</div>
)
}
const ProductCardView = ({
item,
config,
theme
}: any) => {
return (
<a>
...
<CustomSticker item={item}/>
...
</a>
)
}
DOM/BOM API NOT SUPPORTED IN IE
documentElement.prepend - https://developer.mozilla.org/ru/docs/Web/API/ParentNode/prepend
Updated about 1 year ago