The goal of this document is to present a set of guidelines, conventions and best practices to develop frontend code for IDseq. It is an ongoing effort as opposed to a thorough document. These guidelines should be enforced for any new PRs.
When in doubt, follow the Airbnb JS Style Guide.
The following rules are particularly relevant to idseq-web
.
Use const
to define local references and let
if you need to reassign them. Do not use var
.
- Identifiers (variables, objects and methods) should be named using
camelCase
notation starting with a lowercase letter - Classes and filenames are named using
CamelCase
notation starting with an uppercase letter - Use descriptive names. Longer descriptive names are preferred to short cryptic ones, e.g.
convertThresholdedFiltersToJson
vsconvThresh
(great if you can be both short and descriptive...) - Names for specific purposes:
- Event handlers: should be prefixed with
handle...
and, e.g.handleThresholdApplyClick
- Event hooks: should be prefixed with
on...
e.g.onApplyClick
- Example:
<Button onClick={this.handleButtonClick} />
onClick
is a hook (kind of like a parameter), which allows a handler likehandlerButtonClick
to be supplied by a client.
- Example:
- Boolean methods: should be prefixed with a verb
is
orhas
, e.g.isThresholdValid
- Event handlers: should be prefixed with
- Quotes: use double quotes (
"
) by default. - Prefer template strings over concatenation:
this string has a ${variable}
over"this string has a " + variable
.
- Always specify type checking of components properties using the
[prop-types](https://www.npmjs.com/package/prop-types)
module. Be as precise as possible. - Airbnb's prop types validators is installed and provides extra validators.
- If your component does not pass any props to its children, always encapsulate your
propTypes
object in theforbidExtraProps
function from theairbnb-prop-types
package.
lodash/fp
provides nice immutable utility functions (see React section below on immutability). We use lodash/fp
exclusively (no lodash
) in order to prevent confusion between the two variants.
See Higher-Order Functions in Lodash for some examples of using lodash/fp
in practice.
lodash/fp
has many useful functions, and you should use them whenever possible to simplify your code.
A good rule of thumb: if you ever find yourself wanting to use a for loop, lodash/fp
can help.
-
Import Aliases: Be aware of our Webpack aliases. Use them to avoid long relative paths (
../../../..
) in imports. -
Group top-of-file imports into external libs, internal shared libs, and internal un-shared libs. For example
import React from "react";
import PropTypes from "prop-types";
import { merge, pick } from "lodash";
import moment from "moment";
import cx from "classnames";
import { Table } from "~/components/visualizations/table";
import GlobeIcon from "~ui/icons/GlobeIcon";
import LockIcon from "~ui/icons/LockIcon";
import cs from "./visualizations_view.scss";
We define the following tree structure for the React components
(changes to the structure will probably be need to accommodate new features but should be carefully considered):
- The
ui
folder contains generic, reusable components. This establishes a layer of indirection to component implementation, allowing us to easily change the underlying implementation (e.g. we currently use an external frameworksemantic-ui
but could decide to switch a component to our own implementation)controls
: contains components that allow the user to control the workflow of the pagebuttons
dropdowns
layout
: contains components that allow you to organize content on a page (modal, buttons)icons
: react components that encapsulated svg icons
visualizations
: components for data visualizations. Most of these will be using d3. This components should be generic and not attached to any particular type of data, e.g.Heatmap
does not need to be applied to samples comparisonviews
: these are IDSeq specific views likeSamplesView
. These are customized to a particular set of data and are not generic (they are specific to IDSeq). Views can be composed by other views.
If a React component required the use of utility Javascript classes, there are two options:
- If the utility class is to be shared among components, consider putting it in
components/utils
. - If the utility is built specifically for a given component, define a new folder
<ComponentName>
where the component was previously located and move the component and the utility class into that folder.
Try to use the reusable components in ui
whenever possible.
If you need a custom component for your view, see whether you can wrap an existing component instead of writing your own.
For example, if you need a specially styled button, try to wrap <Button>
from ui
instead of creating your own <button>
element.
- Avoid monolithic components. Break complex components up into smaller units.
- No strict guidelines for how to do this, but it's similar to the process of breaking up a complex function into smaller parts. Find small self-contained units. For example, a component whose job is to receive data and render a particular piece of UI. Strive for each component to have a single, well-defined responsibility.
- This makes code easier to reason about and encourages reusability.
- Components should usually be no more than 250 lines.
- Pull business logic out of the component and into pure utility functions whenever possible. Better readability, and easier to unit-test further down the line. Also keeps the component slim and focused on the core business logic and rendering.
- If you find yourself passing a ton of props into a component, consider combining related props into a single object with a descriptive name. For example, if you have a bunch of sample-related props that you're passing into a phylo creation modal, combine them into a single object prop called phyloCreationSampleProps.
- You can also use this approach to reduce the number of top-level this.state variables.
- See (https://github.com/chanzuckerberg/idseq-web/tree/master/app/assets/src/components/views/report/SampleDetailsSidebar) for a real-life example.
Resources: Thinking in React Presentational and Container Components
- Include the type of the component as a suffix of the name. For instance:
PrimaryButton
SamplesView
HeatmapVisualization
If your component is simple and has no styling, a single .jsx
file will do.
If your component has styling, you should create a .scss
file in the same folder as the .jsx
. (Also see styling section below)
Once your component has more than a single .jsx
and .scss
file, you should create a separate folder to hold your component. Follow the convention below:
Table
index.jsx
Table.jsx
Table.scss
Header.jsx
Header.scss
Row.jsx
Row.scss
Where the index.jsx
file is a simple redirect:
// Contents of index.jsx
export {default as Table} from "./Table";
Views represent a page or a section of a page in IDSeq. Views are stored in the views
folder.
When designing your view, try to make as much use of the components in the ui
and visualizations
folders. This should be mostly plug-and-play components.
If you need to place a ui component in a customized place for your view (i.e. if the layout elements cannot help you), wrap it in a div
with a proper class name, and style it in the view's css file.
Views content should be inside of a NarrowContainer
for standard width.
API calls
Put API calls in /api
. Use and build upon the provided api methods like postWithCSRF
and get
instead of directly using axios
. This layer of indirection allows us to do things like standardized error handling and converting snake case to camel case for all our API endpoints further down the line.
Start putting fetch methods like fetchSampleMetadata
in /api
, so that it's easy to see all the back-end endpoints the front-end is using.
When passing complex objects as props, add the structure of the object to a propTypes.js
file. If the object is ad-hoc and very specific to the component, put the propTypes file inside the component directory. If the object will be used widely across the app, put it in utils/propTypes.js
Proptypes should be alphabetized unless there is a natural grouping, in which case you should add a comment explaining the grouping. For example:
Table.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
dataKey: PropTypes.string.isRequired
})
).isRequired,
data: PropTypes.array,
defaultColumnWidth: PropTypes.number,
defaultHeaderHeight: PropTypes.number,
defaultRowHeight: PropTypes.number,
sortable: PropTypes.bool,
sortBy: PropTypes.string
};
-
Don't Mutate Objects: Always create a new object/array when modifying component state, instead of modifying the original object (even if you call
setState
afterward). This allows React to figure out if props have changed by via shallow comparison, and allows for future rendering optimizations usingReact.PureComponent
.lodash/fp
functions are immutable and do this by default. -
Use Arrow Functions: Inside React components, define instance methods with arrow function syntax
foo = () => { ... }
. This removes the need forthis.foo = this.foo.bind(this)
. -
Be Conscious of setState: Be very careful of the the asynchronous nature of
this.setState
, as it can lead to subtle bugs. TheprevState
version (this.setState(prevState => {}
) can work as a quick fix, but overuse ofprevState
and wrapping entire functions inthis.setState
is discouraged. You can often reorganize your functions to remove the bug (and also clarify the different code flows).
WIP D3 code should be created in plain JS (no JSX) and should be placed in separate files for React code.
- We use one scss file per React component, which we place in the same directory as the React component.
- If you need to create related components with common styling (like variations of a component), create a base component that holds all relevant common characterisitcs. The other components will compose this base component. For instance, we defined
PrimaryButton
andSecondaryButton
render aButton
and common properties are defined inbutton.scss
(even if Button does not do much more than rendering the equivalentButton
class fromsemantic-ui
).
- SCSS Filenames should use the
snake_case
notation by splitting words with underscores - Selectors should use the
camelCase
notation.- The choice for this notation is driven by our decision to use CSS Modules (see next bullet). Legacy code uses
dash-case
typically.
- The choice for this notation is driven by our decision to use CSS Modules (see next bullet). Legacy code uses
- We use CSS Modules to modularize our CSS. This allows us to use short class names without fear of collisions with 3rd party libraries or classes from other components.
Be aware of shared style files, particularly _color.scss
. Only use color hex values from _color.scss
and when you need a new color, add it to the file.
- Don't use
materialize
: We are trying to get rid of it. - Flexbox can be very helpful for centering and aligning elements, as well as styling elements to fill up available space. Something to be aware of.
- Use camelCase for properties in JSON object (even if writing in Ruby).