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:

  1. Open devtools (press f12)
  2. Open Network tab
  3. 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 like can'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. use str1.toLowerCase() === str2.toLowerCase().

For example:

// check if the item has the "sale" tag, but the actual tag value might be "sale"/"SALE"/"Sale" ...
{
    ...,
    withPropsOnChange(['item'], ({ item }) => ({
        isSaleItem: item.get('tags', []).find(t => t.toLowerCase() === 'sale')
    }))
    ...
}
// check that item tags contain the string "super"

// not optimized solution - will compute the prop on each component re-render
withProps(({ item }) => ({ 
    hasSuper: item.get('tags', []).find(t => t.toLowerCase() === 'super')
}))

//optimized solution - will compute the prop only when the item has changed
withPropsOnChange(['item'], ({ item }) => ({
    hasSuper: 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
// using HOC
import { withPropsOnChange } from 'recompose';

const enhancer = withPropsOnChange(['item'], ({ item }) => ({
    displaySticker: item.get('tags', []).find(t => t.toLowerCase() === 'new')
}))

const ProductCardView = enhancer(({
  item,
  config,
  theme,
    displaySticker
}: any) => {
  return <a>
        ...
        <Sticker display-if={displaySticker}/>
        ...
  </a>
})

// using React Hooks
import { useMemo } from React;

const ProductCardView = enhancer(({
  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
compose(
    ...,
    withProps(({ item }) => ({
        booleanFlag: item.get('tags').includes('tag a') ? true : item.get('tags').includes('tag b') ? true : false
    }))
    ...
)

// 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
}

compose(
    ...,
    withProps(({ item }) => ({
        booleanFlag: getBooleanFlagByTags(item.get('tags', []))
    }))
    ...
)
- **`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) // will not work in IE

// 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 = enhancer(({
  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 StickerFactory = ({ tags }) => {
    const stickerComponent useMemo(() => {
        if (tags.find(t => t.toLowerCase() === 'super')) {
            return <SuperSticker/>
        }
        if (tags.find(t => t.toLowerCase() === 'sale')) {
            return <SaleSticker/>
        }
        if (tags.find(t => t.toLowerCase() === 'new')) {
            return <NewSticker/>
        }
        return null;
    }, [tags])

    return stickerComponent;
}

const ProductCardView = enhancer(({
  item,
  config,
  theme
}: any) => {
  return <a>
        ...
        <StickerFactory tags={item.get('tags', [])}/>
        ...
  </a>
})

COMMON HOCs (High Order Components)

  • withItemType - check item type autocomplete/search/smart-collection/recommendations
const withItemType = withPropsOnChange(['config'], ({ config }) => {
    const isAutocomplete = config.get('cssSelector', '').indexOf('findify-autocomplete') >= 0;
  const isRecommend = config.get('cssSelector', '').indexOf('findify-recommendation') >= 0;
    const isSmartCollection = config.get('type') === 'smart-collection';    
    const isSearch = !isAutocomplete && !isRecommend && !isSmartCollection; 

    const isMobile = window.innerWidth < config.get('mobileBreakpoint');

    return {
        isAutocomplete,
        isRecommend,
        isSmartCollection
        isSearch,
        isMobile
    }
})

const ProductView = withItemType(
    ({
        item,
        config,
        theme,
        isAutocomplete,
        isRecommend,
        isSmartCollection
        isSearch,
        isMobile
    }) => <a>
        ...
    </a>
)

DOM/BOM API NOT SUPPORTED IN IE

documentElement.prepend - https://developer.mozilla.org/ru/docs/Web/API/ParentNode/prepend