- Components
- Development Environment
- Deployment
- 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
- EsLint
- Husky
- Storybook
- Grid & Responsivity
- Design
- Implementation 10.1 Promises 10.2 Notifications 10.3 Request
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
.
When creating stories for the components, a few steps needs to kept in mind:
- Provide examples
- 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. - Provide different background colors, if needed
3.1 Keep in my the default colors of the UI elemnent in the stories!
- SOON! Provide testcases
- clone the repo
- hit
npm i
- checkout the
dev
branch. - start local develoment environment by
npm run dev
- 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.
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.
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.
-
We are using
css modules
, but withscss
syntax. Keep thescss
as organizad as possible, try to avoid endless nesting. -
You are not allowed to use
#id
selectors. -
Write as generic components as possible to help reusability.
-
Using
prop-types
are mandatory. Read the docs ofprop-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.
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.
Each component have to be in components
folder.
- 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.
- 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
We are using lowerCamelCase
naming conventons for variable names, and UpperCamelCase
, for components
example:
- Variables (incl
hooks
, and generic functions):
const [isVisible, setIsVisible] = useState(false);
- Controller / Uncontrolled
React
comopnents:
const MenuItem = () => ( ... )
- 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
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 atscs/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
andlast
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
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
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
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.
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,
...
)}
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
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");
};
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.
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>
.
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
.
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
)}
/>
In this section listed some solutions.
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
.
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.
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.
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 });
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