Table of Contents

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.

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:

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:

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:

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:

  1. 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 in constants.js in Candidates, and getCurrentFeatures() in Employers.
  2. Container component looks up the concrete implementation for the variant by checking Variants.js.
  3. 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:

Tooling

Redux Forms to Formik (Candidates only)

Form validation

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)