Skip to content

Instantly share code, notes, and snippets.

@zilahir
Last active May 18, 2020 09:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zilahir/ab7c1fbc6e572936f75dc4ff574100a9 to your computer and use it in GitHub Desktop.
Save zilahir/ab7c1fbc6e572936f75dc4ff574100a9 to your computer and use it in GitHub Desktop.
guide

React Guide

Table Of Contents

  1. Components
  2. Development Environment
  3. Deployment
  4. General rules 4.1 Hooks 4.2 Project structure 4.3 Naming conventions 4.4 SCSS & Styling 4.5 Styled Components 4.6 Redux 4.7 Redux-persist 4.8 Animations 4.9 Pull-Requests
  5. EsLint
  6. Husky
  7. Storybook
  8. Grid & Responsivity
  9. Design
  10. Implementation 10.1 Promises 10.2 Notifications 10.3 Request

Components

The implemented components has to have an instance in the storybook. Storybook automatically launched when starting the development environment using the command npm run dev.

Storybook

When creating stories for the components, a few steps needs to kept in mind:

  1. Provide examples
  2. Provide documentation 2.1 The docs needs to be written in markdown and should containt the information you wish to share with other develoeprs regarding the component in the story.
  3. Provide different background colors, if needed 3.1 Keep in my the default colors of the UI elemnent in the stories!
    1. SOON! Provide testcases

Development Environment

  1. clone the repo
  2. hit npm i
  3. checkout the dev branch.
  4. start local develoment environment by npm run dev
  5. you have to have a .env variable file with the following values:
# key value<localhost> value<remote:dev> value<remote:prod>
1 API_URL

Different environtments has different .env files.

For staging it's /.env.staging, for production it's .env.staging.

Both files are located at ./.env.staging.

The different env files are handled by env-cmd during build time.

Deployment

This applicatino is deployed to an AWS S3 bucket. The deployment flow is the following:

The production branch is the master. Direct commits are disallowed to this branch, only Pull Requests are possible..

There is a production pipeline on this branch, which will build the application automatically, but the deployment will happen manually.

The development branch is the dev branch. Direct commits are disallowed to this branch, only Pull Ruqests are possible.

General rules

We are not using class based components, only stateless function components. You must follow the rules set by the provided ESlint configuration. husky package is applied, so you will not be able to commit to this repository if the requirements are not met.

Example:

/**
 * @author zilahir
 * @function MenuItem
 * */

const MenuItem = () => <div>MenuItem</div>;

export default MenuItem;

NOTE: Please comment your components under the import section, so it's purpose will be clearly stated.

  1. We are using css modules, but with scss syntax. Keep the scss as organizad as possible, try to avoid endless nesting.

  2. You are not allowed to use #id selectors.

  3. Write as generic components as possible to help reusability.

  4. Using prop-types are mandatory. Read the docs of prop-types here if you are not familiar with it.

Example:

Button.defaultProps = {
  className: styles.button,
  containerClassName: styles.buttonContainer,
  icon: null
};

Button.propTypes = {
  className: PropTypes.string,
  containerClassName: PropTypes.string,
  icon: PropTypes.node,
  onClick: PropTypes.func.isRequired,
  title: PropTypes.string.isRequired
};

Try to avoid installing 3rd party react components, unless it's a must. Try to implement everything by your own. Let's avoid getting into a dependency mess.

Hooks

Since hooks has been introduced to react, let's aim to use them.

For example:

import React, { useState } from "react";

const [isVisible, setIsVisible] = useState(false);

and when you need to set it's value

setIsVisible(true);

Read more about react-hooks in it's documentation

For hooks follow the naming conventions, as in the example above. If a variable needs to be set to a specific value, use the set in the naming, so it will be consequent, and readable.

Project structure

Each component have to be in components folder.

  1. in common/<component-name>, if it's a common component.

We only using default exports, though aliases are allowed while importing. This helps keep the naming conventions, and refer the compnent by it's name, which helps the readability.

  1. in pages/<page-name>, if it's a page.

The filenames and foldernames should be in the following format:

<SomeComponent> / index.js / SomeComponent.module.scss

Naming conventions

We are using lowerCamelCase naming conventons for variable names, and UpperCamelCase, for components example:

  1. Variables (incl hooks, and generic functions):
const [isVisible, setIsVisible] = useState(false);
  1. Controller / Uncontrolled React comopnents:
const MenuItem = () => ( ... )
  1. As well as styled-components:
const PreviewImage = styled.img`
    backgrond-image: `${props => props.bgImage}`
`

❗ The styled-components and it's usage in this project will be discussed in this document in a later section

SCSS & Styling

The scss modules are always important for a specific component as styles.

import styles from ./TopHeader.module.scss

and then referencing them as:

<div className={styles.topHeaderWrapper}>...</div>

To combine classNames we will use classnames. It's an older approach, but handles null values well.

Example:

<Header className={(
		styles.headerWrapper,
		isSelected ? styles.selected : null,
	)}
/>

mixins, scss variables, functions, nesting.

The reason behind using scss modules, instead of the old css is these mentioned advantages.

The scss files, that are not component related need to be stored at scs/styles/... folder.

Let's use scss variables to declare variables for every usage, case and purposes possible, including padding, sizes, border-radius, etc. This will have a huge impact in the future, when it comes to implementing a new module, or even talking about a complete redesign.

The naming conventions for scss variables are the following:

$unread-notification-border-color: #ffffff;

The scss linter will throw you an error if you are not following.

Examples:

πŸ‘Ž will fail:

$someVariable: 1px

Reason: camelCase is invalid convention in scss variabes.

πŸ‘Ž will fail:

$some-variable: #FFF;

Reason:

Short hex length is not supported.

πŸ‘Ž will fail:

$some-variable: #FFFFFF;

Uppercase hex value is not supported.

πŸ‘ will pass:

$some-variable: #ffffff;

Reason:

Hex value is long, and lowercase.

bariables.scss file can be found here

Let's define and use scss mixins for recurring styling purposes. These purposes can be text related (<p>), list-related <li> or anything like that.

Recommended to use default arguments when defining mixins that encourages even more the reusability. In other cases, you can just use it together with @extend.

To read more about visit the scss documentation

Nesting is one of the most important funcionality in scss. It helps structuring the scss code, gives a readable logic.

Example:

.container {
  .header {
    ...;
  }
  .content {
    ...;
  }
  .footer {
    p {
      &:first-of-type {
      }
      &:last-of-type {
      }
    }
  }
}

Notice the first and last selectors. If you have a DOM like that, you don't need to give class like .first and .second do those p elements, you can use the really well working selectors, such as in the example.

When defining colors, use the hex values, even if alpha value is intended to be used.

Exaple:

background-color: rgba($color: $overlay-bg-color, $alpha: 0.7);

We are using node-sass when it comes to SCSS modules, which inludes autoprefixer, so prefixes should be ignored.

πŸ‘Ž will fail:

.someClass {
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
  background-size: cover;
}

πŸ‘ will pass:

.someClass {
  background-size: cover;
}

Styled Components

styled-components are part if this repository also, use it if a component requires styles related props, such as color, width, etc.

import styled from 'styled-components`

const DemoStyledComponent = styled.span`
    display: flex;
    &:before {
        content: '';
        width: ${props =>  props.width}px;
        ...
        ...
        ...
    }
`

Don't forget, that you can write conditions inside a styled components, so you don't need to define several component for a single purpose, and then pragmatically render them using conditions in the render() method.

Example:

const ConditionalComponent = styled.span`
	background-color: ${props => props.color};
	&:before {
		content: '${props => (!props.isVisible ? `#${props.someOtherProp}` : 'unset')}';
	}

And then referencing them as the following:

<ConditionalComponent isVisible={isVisible} someOtherProp={someOtherValue} />

Using the !important rule in CSS is strictly forbidden. If you come accross a problem which can be solved by using !important you need to redefine your CSS, or your component or both.

Follow the rules of HTML tag nesting, to avoid validateDOMnesting(...) warnings.

ESLint

ESLint is one of the most essential dependency in this project. It ensures you to follow the most generic conventiosn while implementing different tasks throughout the project. This project using a really generic ESLint configuration. Using a proper IDE it will help you with guidance, shows you the errors you've made in you code, helps you fix them, and shows you the corresponding documentation.

ESLint also helps us to follow the very basic rules of HTML, like accessibility, illegal DOM nesting as such.

For example, links has to be links, (<a>), buttons has to be buttons (<button>).

πŸ‘Ž will fail:

<span onClick={() => history.push("/target")}>click me</span>

The reason is becasue this seems to be a link, so it has to be an anchor element.

πŸ‘Ž will fail:

<div onClick={() => toggleMenu(!isMenuOpen)}>toggle menu</div>

Reason: this seems to be a button, so it has to be a button.

Of course, HTML elements can turn to buttons, for example if we want ot achieve something which would lead to illegal DOM nesting with the default elements, but in this case the fundemantal properties needs to be present.

πŸ‘ will pass:

<div
	onClick={() => handleUserMenuOpen()}
	onKeyDown={() => handleKeyPress}
	role=BUTTON
	tabIndex="-1"
>

Husky

Husky will prevent you to push codes with error to the repository. Husky is nothing more then git hooks done well.

❗ TODO: move husky to bitbucket pipeline, so the checks will not run locally.

Redux

Even though we've moved onto React hooks, since react is constantly evolves, Redux is still the best choice for predictable state management.

How we are using Redux in this project?

To introduce new reducer, we combining them in configureStore file. Important to note, that for reducers we are using named exports, so not default as for the components.

This helps us to keep the reducer's name consistent, but combine nem in combineReducers method with a meaningul name. Important to note, that in combineReducers we use shorthand object rules.

Example:

const rootReducer = combineReducers({
	testReducer,
	auth,
	...
)}

Redux persist

We are building a large, scalable application, that strongly relies on microservices. To avoid cahining Promises together, that manages the data flow in the application, we will use persistable redux. This approach adds a persisReducer and persistStore to the redux setup.

redux-persist has several different implementation, to best for us to fit into this project is definitely the one named redux-persist.

When using redux-persist, for resource saving purposes, we will use nested persists, to avoid persisting everything in our redux store. Persisting a whole redux story is never what we want to do.

If you are not familiar with redux-persist go ohead and read their documentation

Animations

The application we are implementing has a lot of microinteractions, whith different animation. Most of them needs to be controllable. After some research, the best toolkit we can use for these purposes is framer-motion, which is production ready, and has a huge open source community behind it. But what's the most important it's build on hooks already.

Example:

import { motion, useAnimation } from "framer-motion";

const controls = useAnimation();

const animation = {
  start: {
    x: 50,
    opacity: 0
  },
  end: {
    x: 0,
    opacity: 1
  }
};

onClickHandler = () => {
  controls.start("end");
};

Pull Requests

The master branch is closed, you shouldn't therefore you are not allowed to push commits there. Every task that is described in jira, has to have it's own branch, and when it's considered as done, a PR to the dev branch should be opened. Code reviews shall happen with each other's branches. The purpose of code review is to help each other.

Storybook

Every component has to have a related story in the storybook. The purpose of this:

To have a clear idea what type of components we've already implemented, see (and try them) in action, understand how it works, so they need to be documented also, list the props, (required, not-required), etc.

The stotrybook as of today (10.03.2020) runs simultaneously with the application within this repository. To start the storybook, hit:

npm run storybook

and navigate to localhost:<port>.

Structure of stories

The stories are located at ./stories folder. The stories inside has to follow the name of the (functional) component was exported with.

For referecne check the story implementiton of the <Button /> story. Located at: stories/Buttons.js.

Grid & Responsivity

To have the best and most powerful tool in our hands, just like with the styling we will combine diffent approaches.

We are using react-flexbox-grid system in this project. More particularly this one. This is not different then most of the other grid systems. It's clean, does not messes up the the DOM with inline styles, because that's something we are not doing.

To make the application and the codebase in general more powerful let react-responsive help. This is a package that supplies API for mediaQueries.

It's built on hooks.

Example:

import { useMediaQuery } from "react-responsive";
const isTabletOrMobileDevice = useMediaQuery({
  query: "(max-device-width: 1224px)"
});

// now you can use isTabletOrMobileDevice variable

For example, together with classnames we can do something like:

import classnames from classnames

<div className={classnames(
	isTabletOrMobileDevice ? styles.someClass : styles.someOtherClass,
	...rest
)}
/>

Implementation

In this section listed some solutions.

Notifications

Notifications are hooked to functions.

There is a common <Notification> component, but it's not needed to import anywhere (it's a root level HOC component added to the App.js).

Usage of the notification:

The notification has a function that can be called with options arguments Object{} when a notification needed to be shown.

Example:

function changeLanguage(newLanguage) {
  setLanguage(newLanguage.lng);
  toast("demo", {
    position: toast.POSITION.BOTTOM_RIGHT,
    type: "success",
    autoClose: 3000
  });
}

This is the most generic example, where inside a function's body calling the notification's own function.

The documentation of the package used for showing notifications can be found here.

If a function can have 2 outcome states (succss | error), we can hook the toastify for example inside the reject callback of a Promise.

The storis of the implemented notifications can be found in the storybook.

Promises

In the implementatino of xenon project, wre using Promises.

Promises are used in the HTTP requests, as well as in the action reducer's dispatch events. The reason for these is to be able to manage the state of the actions and requests, and provides the ability of chaining Promises.

Requests

To send requests to the API, there are dedicated wrapper functions, located at:

src/utils/requests.js

For different methods there are different requests exported.

Method: Get

To send GET requests use the cloudFnGet as the following:

cloudFnGet(path: string<required>, [params:object, options:object])

The API which requires authentication requires the options parameter to be present with the following format:

{
  withToken: true, authToken;
}

Example:

cloudFnGet(path, options, { withToken: true, authToken });

Method: Post

To send POST requests use the cloudFnPost as the following:

cloudFnPost(path:string<required>, data:object<required>, options:object, schema:JoiSchema<required>)

Notice the JoiSchema type at the end of the function call.

This is mandatory to validate the data being sent to the API while performing POST requests.

Every Schema needs to be defined and exported at src/utils/schemas/requests.js

Example:

cloudFnPost(apiEndpoints.auth, authObject, {}, loginSchema);

where loginSchema validates the data being sent to the authorization API endpoint, and looks the following:

export const loginSchema = Joi.object({
  username: Joi.string().required(),
  password: Joi.string().required()
});

While performing POST requests, the schema parameter needs to be present, otherwise the request will throw an Error.

Reason behind this:

To have type safe functions, and requests. Therefore, when something being modified in the backend, or something being modified in the frontend, the debug process will be way faster, easier and more slef explanatory.

The wrapper functions returns a Promise which will support chaining them together, if needed.

Example:

cloudFnPost(apiEndpoints.auth, authObject, {}, loginSchema)
    .then(() => (
        // return something else
    ))

to be continued

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment