Skip to content

Project coding standards and conventions

James Calcaben edited this page Apr 2, 2019 · 10 revisions

Table of Content

File naming and directory structure

  • 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 lower 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 an HOC 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.

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. Thse 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.

Clone this wiki locally