Skip to content

Project coding standards and conventions

Stephen edited this page Mar 17, 2020 · 10 revisions

Project coding conventions

File naming and directory structure

Library code must be in a <root>/lib folder

Most of the packages in this project are libraries. They export code that other apps and libraries can import and consume. For example, peregrine is all library code.

Packages containing library code are not responsible for building that code into browser-friendly bundles or transpiling it for older JavaScript engines. The final consuming app is responsible for that. In this repository, venia-concept is the final consuming app.

All code meant to be imported into other code should be put in a lib/ directory at package root.

Source code for the final consuming app must be in a <root>/src folder

The final consuming app is the root of the dependency tree. It collects all the library and module source code and puts it together into an app which renders a webpage.

To render a webpage quickly and on most browsers, the source code must be built into optimized "bundles" using the Buildpack pipeline, Babel, and Webpack. All code meant to be bundled should be put in a src/ directory at package root, unless that code is imported from another module or package.

Build output for the final consuming app must be in a <root>/dist folder

The final consuming app builds code bundles that must be served by the UPWARD server or another web server. This code will be deployed, uploaded to production systems, and distributed to the devices and browsers of site visitors.

All code meant for direct download by users should be put in a dist/ directory at package root. Buildpack is configured to output build assets to this directory by default.

JavaScript files must use the .js file extension

This project does not use TypeScript or ECMAScript modules, so .ts and .mjs extensions are not used. The .jsx extension is unnecessary because all files are processed for JSX.

GraphQL files must use the .graphql extension

The build process parses and analyzes GraphQL files based on this extension.

Filenames must be in camel case

Example: camelCasedFilename.js

This is done for consistency and readability. Even files defining a component, such as the Button component, must have camel case file names.

React component folders must use proper case

Example: ProperCaseDirectory

Components must always be directories and never single files. The ProperCase indicates that the directory is a component.

Names for .css and .js files for the same component must match

Example: checkbox.css and checkbox.js

This is done for consistency, readability, and future extensibility.

Use a container.js file to wrap simple, presentational components with a Higher-Order Component

Presentational components are meant to be simple and portable. They require only props as arguments and no other external connections.

Wrapping these components with Higher-Order Component allows them to connect to the application's router, Redux store, and network/cache clients.

Do not use underscores and hyphens in names

Data from the GraphQL API is snake_case and all other property and variable names are camelCase. This helps to visually distinguish between internal variable names and data objects from the API.

React components

Each React component is contained in a single folder

React components must be independent and interchangeable. Their tests, sub-components, stylesheets, and utilities should be self-contained. They should not be reliant on other components without explicitly naming them in import strings.

A component's public API is exposed through its index.js file

The index.js file is a common standard in a React project. Naming a file index.js allows a component to import another component using its directory name.

// src/components/Button/index.js

export { default } from './button';
// src/components/Form/form.js

import Button from 'src/components/Button'

In the example provided, the Button component exports its components in it's index.js file and the form.js module imports that button by only referring to the directory.

Text editors can become confusing to navigate when files with the same name are open. Therefore, the index.js file should be a short (one or two line) "pass-through" file which exports a named sibling file.

A component typically exports only a default export, which is the component constructor itself.

Components not exported through index.js are considered private API

If a component author exports another public file, such as a sub-component or a utility, the index.js file is the place to include the export.

// src/components/Button/index.js

export { default } from './button';
export { default as MultiButton } from './multiButton';

This guarantees that Webpack can bundle the minimum amount of code possible.

Sub-components are defined in files in a component's root directory or created in their own folders

Use sub-components to define complex components. A sub-component defined inside a parent component's folder is considered a private class.

Not all sub-components need to be put in separate directories nor made public.

Don't import a sub-component directly from a component folder.

// src/components/Form/form.js

import InnerButton from 'src/components/Button/innerButton'; // Don't do this!

Create component tests in the __tests__ folder

The Jest testing framework, which is the recommended PWA Studio test framework integrated with Buildpack, searches for tests by default in folders called __tests__. It will find these folders in any subfolder.

Keep tests for a component inside that component's folder to preserve isolation.

Create storybook tests in the __stories__ folder

The Storybook framework, which is the recommended self-documenting component playground for PWA Studio, searches for story files by default in folders called __stories__. It will find these folders in any subfolder.

Keep stories for a component inside that component's folder to preserve isolation.

Talons

Talon file definition

A talon is defined in peregrine in a parallel path to venia-ui. For example, a component defined at

/packages/venia-ui/lib/components/MyComponent/...

would have talons defined at

/packages/peregrine/lib/talons/MyComponent/...

Talon signature

A talon accepts a single object as an argument and returns a single object:

const talonProps = { ... };
const { ... } = useMyTalon(talonProps)

Passing GraphQL operations to a talon

Pass queries and mutations to a talon with queries and mutations properties:

useMyTalon({
    queries: {
        myTalonQuery
    },
    mutations: {
        myTalonMutation
    }
})

Root components

Root components are defined in the RootComponents directory

The RootComponents directory is specifically named in Webpack configuration.

The Buildpack MagentoRootComponentsPlugin searches the src/RootComponents folder for files containing comment directives to identify themselves as root components.

Root components are defined with comments that identify them as entry points for a page type

A RootComponent should be used to render the overall UI for an entity page (i.e. a product page with a routable URL). RootComponents are designed to handle particular entities, so they must declare this compatibility in their comment directives to help the MagentoRootComponentsPlugin use them when necessary.

Root components are associated with server routes

A RootComponent renders a "page", or an overall UI state, in the PWA. These large state changes should correspond with navigations between routes that are linkable.
This may be managed by the server's SEO configuration.

Client state management

Use Redux for global state and as a client side store for optimized data

Storing data using Redux is done for expediency because it comes with well-understood patterns for adding new functionality and state management, making asynchronous requests, and and storing the results of those requests.

Note: The PWA Studio project will eventually remove Redux and replace it with a more consistent GraphQL state management system so that server-side state and client-side state can be handled in the same way.

Redux actions and reducers are members of a set of "store slices"

Store slices, which are named sections of store functionality, are a way of encapsulating independent business logic in client state management.

For example, cart-related state changes are in the cart slice.

Slices are implemented by dividing actions and reducers into subfolders and files named after that slice.

Slices can depend on each others' state through actions which can dispatch actions from other slices. The app slice handles any state values not associated with a particular slice.

Redux actions are defined in the actions directory

Actions are objects with a specific purpose in Redux, and Action creator functions have a particular signature, which should be grouped together.

However, actions are properties of the app, not of individual components, so the actions directory should live alongside the components directory.

Redux actions must be placed in subdirectories in the actions directory

Action subdirectories are named after store slices, such as cart or category. Redux actions must belong to a store slice, a named section of store functionality that implements all logic for that functionality.

These subdirectories also have a standard structure.

Follow Flux Standard Action standards when naming and defining actions

The Flux Standard Actions specification helps keep action definitions standard.

Actions should be plain JavaScript objects that can be serialized into JSON. This allows them to be used as test fixtures, and sent across window boundaries via postMessage.

They must always have type, payload, and error properties, and optionally a meta property. This allows polymorphic treatment of action objects by reducers and other functions which need to operate on them.

Use the redux-actions library to create and handle actions

The redux-actions library helps authors maintain a list of action type strings and handlers. These can be reused throughout the project.

The prefix for an action type must be unique and in all caps

Action types in Redux must be in ALL_CAPS, for easier visual identification and consistency. When configuring a "prefix" in a call to redux-actions' createActions, make sure it's in all caps.

Use the createActions() function to create an actions object from a list of action types

The createActions() method allows the definition of multiple namespaced actions without the additional boilerplate of manually implementing their factory functions.

Action files are separated into synchronous (actions.js) and asynchronous (asyncActions.js) actions

Synchronous actions are always handled fully synchronously by the store slice. They perform no side effects on dispatch. They rarely need custom implementations because the functions created by redux-actions are sufficient in almost all cases.

Asynchronous actions perform side effect. They usually by make network calls and dispatch their results as payloads. These actions require custom implementations.

Asynchronous actions can import actions from different domain slices

The approved way for store slices to interact with each other is through actions importing other actions.

For example, the checkout store slice handles the state management of a checkout flow. When that flow completes, the cart slice must reset itself.

Therefore, the src/actions/checkout/asyncActions.js file must import src/actions/cart/ to dispatch the appropriate action.

Action names are public API

There is no way to keep actions private.

Action type names are visible throughout the entire application, as a principle of Redux. Reducers have access to these actions, and any component can use the connect() binding of react-redux to acquire a handle on those action creators.

Redux reducers are defined in the reducers directory

Reducers work together with actions.

Actions are dispatched to the Redux store to indicate a potential state change. Reducers perform that state change by taking the action along with the old state and returning a new state.

They must always be named after their store slice, e.g. src/reducers/cart.js.

Reducers must be synchronous by default, so instead of two separate files, they can be implemented in one.

State objects should never be mutated

Always return new state objects.

Redux works best when all state objects are immutable. This allows a component to determine a change in state by checking simple reference identity. This prevents it from receiving a false negative.

If a reducer reuses an object and modifies its properties, a component trying to compare an old state with a new state object may get a false positive.

Middleware

Redux middlewares and store enhancers are located in the middleware directory

Redux enhancers and middlewares add custom global functionality to the Redux store.

Our middlewares include:

  • redux-thunk - a utility for dispatching asynchronous actions and results
  • redux-log - used for logging state information to the console in dev mode
  • A "backstop reducer" - used for handling unexpected errors with a fallback UI

It may become necessary to modify or add Redux middleware when building a project If it becomes necessary, put the middleware here.

Application drivers

Application drivers are located in the drivers directory

Drivers are code units that connect React components to the "outside world", the state of the application, and network. This concept goes beyond the props sent to the component and its internal state.

Drivers include API clients, router components for interacting with a router, and higher-order components that connect components to the Redux store.

All of these code units should be centralized in src/drivers/index.js.

Often they will be pass-through exports of the underlying libraries, like react-router and react-redux. Putting them in one place helps a downstream user override one or more of these drivers for custom behavior and testability using a minimum amount of configuration.

Application drivers should include an Adapter component which connects a React application to all drivers

The current driver utilities rely on ancestor components to work.

For example, a Link element requires an ancestor react-router component, a Query element requires an ancestor apollo component, and a Redux connection require a Redux store registered at the top level.

If possible, create a single component which adds all those dependencies and then render its children.

Apollo GraphQL

Mutation PII Cheat Sheet

Here's a handy mutation PII cheat sheet:

Outgoing parameters contain PII? GraphQL response updates a cache item? Solution
T T Use the @connection directive
T F Set fetchPolicy to no-cache
F T Don't use no-cache; The @connection directive is optional (not needed)
F F Set fetchPolicy to no-cache

GraphQL File Location and Naming

Assuming a component named Foo, fragments are defined in fooFragments.gql.js and queries/mutations/resolvers in foo.gql.js.

components
  Foo
    foo.css
    foo.gql.js
    foo.js
    fooFragments.gql.js
    index.js
Clone this wiki locally