-
Notifications
You must be signed in to change notification settings - Fork 685
Project coding standards and conventions
-
Project coding conventions
-
File naming and directory structure
- Library code must be in a
<root>/lib
folder - Source code for the final consuming app must be in a
<root>/src
folder - Build output for the final consuming app must be in a
<root>/dist
folder - JavaScript files must use the
.js
file extension - GraphQL files must use the
.graphql
extension - Filenames must be in camel case
- React component folders must use proper case
- Names for
.css
and.js
files for the same component must match - Use a
container.js
file to wrap simple, presentational components with a Higher-Order Component - Do not use underscores and hyphens in names
- Library code must be in a
-
React components
- Each React component is contained in a single folder
- A component's public API is exposed through its
index.js
file - Components not exported through
index.js
are considered private API - Sub-components are defined in files in a component's root directory or created in their own folders
- Create component tests in the
__tests__
folder - Create storybook tests in the
__stories__
folder
- Talons
- Root components
-
Client state management
- Use Redux for global state and as a client side store for optimized data
- Redux actions and reducers are members of a set of "store slices"
- Redux actions are defined in the
actions
directory - Redux actions must be placed in subdirectories in the
actions
directory - [Follow Flux Standard Action standards when naming and defining actions](#follow-flux-standard-action-standards-when-naming-and-defining-actions)
- [Use the redux-actions library to create and handle actions](#use-the-redux-actions-library-to-create-and-handle-actions)
- The prefix for an action type must be unique and in all caps
- Use the
createActions()
function to create an actions object from a list of action types - Action files are separated into synchronous (
actions.js
) and asynchronous (asyncActions.js
) actions - Asynchronous actions can import actions from different domain slices
- Action names are public API
- Redux reducers are defined in the
reducers
directory - State objects should never be mutated
- Middleware
- Application drivers
- Apollo GraphQL
-
File naming and directory structure
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.
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.
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.
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.
The build process parses and analyzes GraphQL files based on this extension.
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.
Example: ProperCaseDirectory
Components must always be directories and never single files. The ProperCase indicates that the directory is a component.
Example: checkbox.css
and checkbox.js
This is done for consistency, readability, and future extensibility.
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.
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 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.
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.
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.
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!
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.
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.
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/...
A talon accepts a single object as an argument and returns a single object:
const talonProps = { ... };
const { ... } = useMyTalon(talonProps)
Pass queries and mutations to a talon with queries
and mutations
properties:
useMyTalon({
queries: {
myTalonQuery
},
mutations: {
myTalonMutation
}
})
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.
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.
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.
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.
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.
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.
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.
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.
The createActions()
method allows the definition of multiple namespaced actions without the additional boilerplate of manually implementing their factory functions.
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.
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.
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.
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.
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.
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.
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.
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
|
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
- Sync calls:
- Check the calendar
- Recordings - https://goo.gl/2uWUhX
- Slack: #pwa Join #pwa
- Contributing
- Product