Refer to https://handbook.glintsintern.com/technical/frontend.html for the latest documentation.
Frontend Projects Contribution Guide
At Glints, we host many frontend projects, ranging from those accessible by the general public to those that are solely for internal use. It is foreseen that there will be a growth in the number of projects. This potentially leads to inconsistencies between projects, leading to reduced ability for us to easily and quickly contribute across codebases.
Therefore, it is beneficial for us to maintain a common set of conventions across these projects. This guide is intended to be a living document. We welcome contributions, feedback and suggestions.
Portions of this document is based on GitLab's Frontend Contribution Guide, which is licensed under the MIT license.
Core Principles
These are few principles that we abide by when contributing to frontend code.
Discuss architecture before implementation
Discuss your architecture design in an issue before writing code. This helps decrease the review time and also provides good practice for writing and thinking about system design.
Be consistent
There are multiple ways of writing code to accomplish the same results. We should be as consistent as possible (naming conventions, indentation rules, etc.) in how we write code across our codebases. This will make it more easier us to maintain our code across Glints.
Enhance progressively
Whenever you see with existing code that does not follow our current style guide, update it proactively. Refrain from changing everything but each merge request should progressively enhance our codebase and reduce technical debt.
Initiatives
This list provides a high level overview of where we are going from a frontend perspective.
- File organization: Organization by file type to organization by feature
- File organization: 1 main language per file
- File organization: 1 component per file
- Filename convention: kebab-case to PascalCase
- Dependency injection: Variants infrastructure
- Maintainability: Magic strings to symbolic constants
- Tooling: Plain CSS/Radium to SASS/styled-components (Candidates only)
- Tooling: React Router v3 to v4 (enables HMR) (Employers only)
- Tooling: Redux Forms to Formik (Candidates only)
- Environment: Universal Rendering
- Environment: Frontend Server
Development
Organization by Feature
Why: This loosely implements the Law of Demeter and the general principle of high cohesion and low coupling at the module level. Components that are closely related to one another are placed closely on the filesystem. This provides the following benefits:
- In React Redux projects, reduces the need of switching from folder to folder
- Reduces potential for significant merge conflicts since each developer generally works on problems by features.
We organize our code by feature epics. One feature epic is a top level module. If sub-modules are desired, they are allowed.
In general, feature modules are located in app/modules/.
Within a feature module:
index.js: Interface boundary between modules. Enumerations are excluded to avoid mutual dependencies.FeatureFlags.js: Optional, contains feature flags that are local to the module.- State manipulation/storage:
Actions.js: Contains all the Redux actions and their action creators. Each action and action creator is re-exported inindex.jsshould other modules need access to them.Reducer.js: Default exports the reducer for the module. Re-exported inindex.jsasReducer.Selectors.js: Selectors for the state tree. Individually re-exported inindex.js, following the same rules as actions.
Example implementation of FeatureFlags.js:
import PropTypes from 'prop-types';
const FeatureFlags = {
BACKGROUND_IMAGE: 'backgroundImage',
METADATA_REFERRAL: 'metadataReferral',
SET_PROTECTED: 'setProtected',
SIGN_UP_FORM_VARIANT: 'signUpFormVariant',
};
const FeatureFlagsPropTypes = PropTypes.shape({
[FeatureFlags.BACKGROUND_IMAGE]: PropTypes.string,
[FeatureFlags.METADATA_REFERRAL]: PropTypes.string,
[FeatureFlags.SET_PROTECTED]: PropTypes.bool,
[FeatureFlags.SIGN_UP_FORM_VARIANT]: PropTypes.string,
});
export { FeatureFlags, FeatureFlagsPropTypes };
Example usage of feature flags:
import { FeatureFlags as SignUpFeatureFlags } from '../modules/SignUp/FeatureFlags';
import SignUpFormVariantTypes from '../modules/SignUp/SignUpFormVariantTypes';
...
const FEATURES = {
glints: {
signup: {
[FeatureFlags.SIGN_UP_FORM_VARIANT]: SignUpFormVariantTypes.PSYCH_FLAT,
[FeatureFlags.METADATA_REFERRAL]: 'glints',
},
...
},
...
};
Common components are located in app/components/. For a component to be
considered a common component, it must fulfill these criteria:
- Re-usable: e.g. more than 1 module needs it. This component may also be context-independent.
- Supports multiple-instancing: If this component relies on the state tree, there should be no issue mounting more than 1 instance of this component within the state tree.
One Component Per File
In general, a component should be isolated to a single file.
One Main Language Per File
In the case where styled-components are used in a component, the style
definitions should be located in another file with a similar name. For example,
MyComponent.js should have its styled-components located in
MyComponent.sc.js and exported, and imported in MyComponent.js.
For shared (common) styles, when appropriate, consider the addition of the
style to the Glints Aries component library. Otherwise, it's perfectly okay
to have a file named styles.js that exports styled-components.
kebab-case to PascalCase
You may see a mix of files that are named in kebab-case.js rather than
PascalCase.js. We're moving away from that.
Variants Infrastructure
Why: Implements dependency injection and supports A/B testing or whitelabelling
Within a module, a container component selects the right component to render via the following process:
- Container component checks the feature list, which references a specific
variant type. The variant types are defined in
VariantTypes.js. The feature list can be found inconstants.jsin Candidates, andgetCurrentFeatures()in Employers. - Container component looks up the concrete implementation for the variant by
checking
Variants.js. - Container component pulls in props passed into itself and from the state tree into the injected component.
In terms of file structure, these files are generally used:
Variants.js: References actual implementations of the component.VariantTypes.js: Enumerates all possible variant types.<Component>Container.js: Dynamically determines the component to be injected and assembles the props for it.
Tooling
Redux Forms to Formik (Candidates only)
- We intend to replace all usage of
Redux FormswithFormikin our candidates codebase for reasons explained here. - For a working example of how to use
Formik, refer to this file in theglints-dstproject.
Form validation
Formikcomes with a special config option calledvalidationSchemawhich works withYup, a library for object schema validation. We recommend usingYupfor form validation instead of writing your own helper functions.
Magic strings to symbolic constants
Whenever you see a value that has potential to change, use a symbolic constant. This applies to both strings and numbers. For example:
get(features, 'hideWorkExperiences', true);
Should be transformed to:
const FeatureFlags = {
HIDE_WORK_EXPERIENCES: 'hideWorkExperiences',
};
...
get(features, FeatureFlags.HIDE_WORK_EXPERIENCES, true);
This allows easy usage search, and in the future, allows type checking with TypeScript.
Frontend Server
We use HapiJS for serving the files. Using HapiJS instead of standard web serving layers like nginx allows us to more easily perform integration testing.
It's possible to use mocha or Jest to easily test out routing behaviour and other logic.
With HapiJS, it's possible to implement a significant subset of the server functionality as plugins.
State Management
We use Redux for managing states. The main benefit of managing state with redux is that the store becomes the single source of truth for representing the UI.
React's local state vs Redux
For majority of the use cases, using redux is the sensible choice as it allow us to test and debug states easily.
For other minor use cases, such as storing state for small UI adjustments whose behaviour are not shared with other parts of the application, local state may be preferred over redux.
Read: When to use react's state vs redux
Dependency Management
Use Yarn
Yarn is our officially supported package management tool. Yarn is significantly faster and more resilient than npm. While npm 6.x does bring many performance improvements, it is still not as resilient to network failures as Yarn is.
Adding Dependencies
To add dependencies, use yarn add <dependency name>. It's best to use the
Yarn provided in the builder Docker image if possible. To do that:
glints dev run --rm --entrypoint sh <project name>
yarn add <dependency name>
If you do not use a Docker environment, just ensure that the Yarn version used is as close as possible to the one used within the container.
Updating Dependencies
To update dependencies, modify package.json with the new version of the
desired package, then re-run yarn. Similar to above, if possible, use the
Docker-based environment to update dependencies.
Updates to dependencies should be done in a standalone commit.
Refrain from re-generating the entire lockfile.
Imports Ordering
When ordering imports, it's recommended to do the following:
import React from 'react'; // React always comes first
// Library imports comes after - note that they are alphabetically sorted by
// module name.
import classNames from 'classnames';
import get from 'lodash/get';
import compose from 'recompose/compose';
// Common imports.
import { whitelabel } from '../components/whitelabel';
// Module-local imports.
import { FeatureFlags } from './FeatureFlags';
This is for consistency reasons.
Style Guides
In lieu of a style guide, we use ESLint rules to help maintain consistent code formatting standards. Our coding standards are based closely on Airbnb's standard, but with some practical adjustments.
Tips (coming soon!)
Syntatic Sugar
A compilation of the recommended shorthands to use in our codebase.
mapDispatchToProps object shorthand
When using redux connect HOC (Higher Order Component), you should use the mapDispatchToProps object shorthand unless you have a specific reason to customize the dispatching behavior.
Reference: Redux Docs
import { push } from 'connected-react-router';
class MyComponent extends React.Component {
...
}
connect(
state => ({
...,
}),
// mapDispatchToProps object
{ push }
)(MyComponent)
You can also customize the actions you want to dispatch:
connect(
state => ({
...,
}),
// mapDispatchToProps object
{ goTo404: () => push('/404') }
)(MyComponent)