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.
This allows to keep all the properties organized for a selector, as you change the appearance of the selector within your media query (change it's property), so keep only properties changing. Additionally, this allows for easy control (add / remove) of properties and other media mixins to the selectors.
.Button {
// Wrong
@include mobile {
&:hover {
text-decoration: none;
}
}
// Correct
&:hover {
@include mobile {
text-decoration: none;
}
}
}CSS variables must be used for color definitions for ScandiPWA.
It provides ability for modern and 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 itself, should be moved out into variables.scss file.
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.
// 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};
}
}Complex styles commonly require multiple pseudo-classes, pseudo-elements, media queries, properties, selector-build-ups. Order matters a lot, as good organization helps to keep it trackable and easily readable.
- Non-content based mixin has to be defined on top, in order to be rewritten with properties later
- Variables must be declared before these are used (according to specs)
- Pseudo-classes and pseudo-elements must not be mixed together
- Element and modifier definitions must be above other definitions (i.e. element definition)
.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;
}
}ScandiPWA follows BEM CSS organization methodology, that provides convenience, ease and good effectivity of the project styles. Each nested selector increase the "weight" of rules, making it harder and harder to override. You can learn more about "weight" or specificy approach: https://css-tricks.com/specifics-on-css-specificity/
// Source code
.Component {
&-Wrapper {
// Element properties
&_wide {
// Modifier properties
}
}
}
// Compiled code
.Component-Wrapper {
// Element properties
}
.Component-Wrapper_wide {
// Modifier properties
}All values must be rounded to integer pixel values, or the pixels should be used when possible. The values of CSS properites should never reference decimal values under a pixel, as it is impossible to draw less then 1px (if it is not a clip path!).
Also, the values like 23px must be rounded to each 5px that improves readability. The reccomened stops are 5px for content-full elements and 1px for simple elements.
0 in front of values lower than 1 is redundant:
- correct:
.25 - wrong:
0.25
Whether element height has a float
pxvalue instead of integer - 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 second element might be offset. A lot of issues might be potentially caused by non-rounded values – avoid using them!
// 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);Using em and rem are often misleading: they are dynamic and you often don't know what value it contains. To make it clear:
remis the body font-size based unitemis the parent element font-size based unit
There are only two rules – make sure you are using values that can be rounded to a full pixel. Make sure you have a reason to use
remorem. In general, most common use case for any of these to be used is text distribution.
body {
font-size: 12px;
}
// Wrong
padding: 1.3rem;
left: 2.35rem;
// Correct
padding: 15px;
left: 28px;
padding-left: 1rem;You must not use !important.
Important is the most powerful form of CSS selector strength rules, it overrides id, class, name, everything. It is very commonly used to override inline-styled elements, but brings inconsistency.
It is much better to use third parties, that escape direct styling, by for example using custom stylesheets and applying CSS custom variables.
// <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.You must not define width / height fixed size.
The recommeded way to define styles is by using box-sizing: border-box along with padding to define element's height.
This prevents from applying custom style to the element, breaks
flexif element is involved in it, and overall complicate styling. It is easy to assume and follow the assumption content size will never change, however such asumption is wrong.
When styling it is 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. In order to achive the effective behaviour of the element when loading use
min-height.
// Wrong
.ProductName {
height: 20px;
margin: 1rem;
}
// Correct
.ProductName {
min-height: 20px;
margin: 1rem;
}It often happens, that default values are defined multiple times, i.e. font-style: normal; or font-weight: normal;, especially, when styles are based on apps like Zeplin.
Define only necessary properties, skipping all default (defined as default) properties.
Never rely on browser defaults, it differs from browser to browser. Use reset.scss for unifying default styles.
// Wrong
font-size: 15px;
font-weight: 300;
font-style: normal;
font-stretch: normal;
// Correct
font-size: 15px;
font-weight: 300;When loading a data, we must display placeholders for the data that is a part of a page (layout).
Placeholders are great for filling the layout while we are waiting for the data, provides suggestions about the content size or type and improve webpage UX. Do not use conditional rendering to showcase this, implement it using early returns and test, test, test!
In perfect condition, if you have not yet recieved a data to be rendered, you must already render the full element structure, but with placeholders inside, this way, after data will be recived, there will be no complex re-renders, just places were placeholders were – this eliminates page blinking.
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
}
}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 suer you are preparing your data well in your reducer or container.
/**
* 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
};You must not reasign variable values, because:
- you may change the external state by an accident
- code complexity is growing
In a paradigm of functional programming, you should never reassign values to variables. The functions must follow Maths way – if
ais passed, the value should be returned, the passed value should never be changed or redefined, as in in most common scenarios:
Read the great article about this!
// Bad
let hair
if (today === 'Monday') {
hair = 'bangs'
} else {
hair = 'something else'
}
// Good
const hair = today === 'Monday' ? 'bangs' : 'something else';You must destruct props / states in the beginning of the method / function. You must not pass descructured data between methods, if fields can be desctructured independently.
Reduce complexity and improve readability, brings clear dependency on the data you are processing:
- Allows for easy reading of what is going on in the code
- Allows for easy default value declaration, without modifying the inital data source
- Allows for varaiable name aliasing The dot notation is great, but when used a lot it brings inconvenience. It is much better pratice to separate objects to smaller parts – destructurizing them into variables.
// 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;// 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>;Using arrow functions is great, but they must be created in runtime. You must avoid this behaviour by binding them to component context. There is also an option to declare them as arrow functions initially, but it is not recommend, as we would like to stay consistent thoughout the project (in terms of function declarations).
class MyComponent extends Component {
constructor() {
this.handleOneMoreClick = this.handleOneMoreClick.bind(this);
}
// Preferred way
handleOneMoreClick() { ... }
// The OK one, but not recommened
handleOtherClick: () => { ... }
// The bad one
handleClick() { ... }
render() {
return (
<>
{ /** Not the right way to go */ }
<button onClick={ () => this.handleClick } />
<button onClick={ this.handleOneMoreClick } />
<button onClick={ this.handleOtherClick } />
</>
);
}
}You must avoid re-declaring values.
You should use map, reduce, filter, find (and others!) over Array.forEach().
// 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;
}, {});You must use early returns.
// 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 code
return something;
}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 name it like you need imidiatelly. It is strange to see abstarct variable names like value, which does not represent anything.
// 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, userNameWhen implementing your own dispatcher classes you might see places, where stumble across a place where multiple dispatch functions are called. You must join them into one action dispatch.
When not merged together, it will become hard to track. On global state update, the component will update multiple times, so tracking previous props in
static getDerivedStateFromPropswill not be easily achievable. Also, the rendering might happen more than one time, that might cause some lag noticeable lag.
// Inefficent
dispatch(updateProducts(products));
dispatch(updateBreadcrumbs(products));
dispatch(updateCategoryFilters(products));
// Great
dispatch(updateProductsAndCategory(products);When implementing a react render method, the JSX is typed, it supports a conditional rendering wrapped within { ... }. This feature is often over-used by developers, which result in very clunky, big render methods.
Decoupling, or, if we rephrase, Dividing complex, conditional renders in to smallerfunctions, makes it:
- More extendable
- More readable
- Allows for easy additional checks
// 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>
);
}Make sure to remove unnecessary or unused tags, comments, commented code, attributes.
// Bad
return (
<>
// <Item item={item} />
<div>
{ /** ... */ }
</div>
</>
);
// Great
return (
<div>
{ /** ... */ }
</div>
);