This document aims to serve as a baseline for the majority of medium to large-scale front-end / JavaScript-based projects in Arbisoft. We've curated the best practices in one place in order to ensure adherence to the quality level, performance, and extensibility of the products we deliver.
Beyond all of the guidelines mentioned here, the most important thing to make an effort for is developing a culture of creating quality among ourselves; the rest will follow automatically.
Note: The recommendations of tools and libraries present in this document are suggested based on their long-standing past experiences and great track record in the industry for their performance and maintainability. Nonetheless, we know that the JS world is continuously evolving with innovation. Therefore if there is any library/tool superior to the suggested ones in this document, please discuss with the committee and we shall incorporate it in our guidelines.
Definitions
Marker | Title | Definition |
---|---|---|
❗ | Highest Priority | Must be implemented or followed. This can not be compromised in any case. |
⚠️ | Medium Priority | Should be implemented or followed. This can only be left over with a fairly justifiable rationale and a future plan to handle later on. |
💡 | Low Priority | Good to Have but can be ignored in the interest of available resources and project priorities. |
Choose a framework for a frontend that can build applications that are able to be used by thousands and millions of users. Keep performance, extensibility, and community support in mind when choosing a framework. Our recommendations of the framework(s) are:
Folder Structure: Which folder structure is ideal is a long-running discussion with varying subjective opinions. It is an important aspect to discuss nonetheless and the experience of working with large-scale applications tells that Component Folder Pattern [1] [2] is the best one in terms of scaling for growing codebases.
Forms: A common pitfall is to implement custom form-handling because it is so easy to do it in React.JS - instead choose a mature & stable library like formik or react-final-form to handle all forms and their respective validations in a consistent, performant manner. (DO NOT use redux-form as its deprecated; react-final-form is the newest library from the same author) ❗
Date and Time Handling: Usually the de-facto choice for handling date and time (w.r.t. to time-zones) in JS apps is moment.js, but it has a huge footprint on the bundle size of the app and is now in maintenance mode. Therefore it is recommended to use new light-weight alternatives like luxon or date-fns instead. ❗
Utility Functions: Lodash is a great utility for working with reference data-types (objects, arrays, strings, nested structures) in order to manipulate or search them. However, it also has a large footprint on the bundle size of the app. Therefore it is advised to use individual lodash functions as packages. This way you can avoid installing the whole library. ❗
Immutability: With the increasing demands of data-driven front-end apps, it is more than ever important to achieve immutability while working with JS reference types to achieve a predictable behavior of the language and avoid hard-to-debug errors. Therefore we recommend using immer. 💡
Use React.JS as View of MVC: Although React.JS provides built-in APIs (State, Context) to work with the data, it is still better to treat React as a View technology, i.e., React part of the codebase should only be responsible for rendering dumb UI. This translates into React code mainly using Presentational (Functional) Components instead of Class Components and being free from any kind of data manipulation. Use selectors (explained later) to perform data manipulation and inject it as props to the respective Presentational Component and dispatch actions to perform any async operations. The biggest benefit of not using React's State for data manipulation is that one won't need to implement the lifecycle hook of React like shouldComponentUpdate to do performance because React will only be rendering dumb UI. [3] [4] ❗
Prefer React Hooks over React Class Components: For using React's Lifecycle hooks, use the new API of React Hooks instead of React's Class components. ❗
Create as many components as possible: Even for the smallest UI elements. ❗
Never use Indexes as Keys: ❗
Define PropTypes of Components: ❗
Lifting UI state in Global Store: The pattern of using React.JS also advocates lifting the state of application's UI elements from React's state into a global state like Redux (with the only exception being Forms), such that there is a single state-tree for the whole application. This also strengthens the deterministic nature of the application. Reference ⚠️
Decompose Reducers: Split up Reducers into maintainable, extensible chunks. [5] ❗
Managing Side Effects with redux-saga: Sagas is hands-down the best option to manage async processes (aka side-effects) for large-scale applications because they can handle async-chaining of side-effects by completely avoiding callback hell (redux-thunks cannot do this without a messy, buggy code). Sagas encourage clean and reusable code with minimum boilerplate and encourage explicitly, yet consistent error-handling of async actions. Sagas provide a very clean way of accessing stores (sorry thunks) and are capable of handling parallel processes, forking, spawning, canceling, and a lot more. [6] [7] [8] [9] [10] ⚠️
Use Selectors (with Memoization): Most of the components will need some dynamic data to be injected as props. For that purpose, an anti-pattern is to subscribe to the store (with connect() binding) and manipulate the props inside the component. This is completely wrong and any local manipulation of the data should be performed in selector functions. One should also use selectors for getting static data into the components, instead of hard-coding the static data in components. [11] [12] [13] ❗
Data Normalization: Implementing DRY in Redux means that one should also manage data duplication in the global store. This means that for large-scale apps, one should treat the application's global storage as a database. [14] [15] ⚠️
Reduce Boilerplate with Ducks: Ever tried scaffolding your types/actions/reducers code? Use Ducks Pattern to minimize the repetitive codebase and ensure naming consistency in the redux entities. 💡
Eliminate unused code with Tree-shaking: Use ES6 Modules to import only required components in your code files, instead of importing default modules as a whole. When using destructured imports Webpack performs tree-shaking and eliminates dead-code in the production build. [16] [17] ❗
Remove render-blocking JavaScript: JavaScript blocks the normal parsing lifecycle of HTML documents, so when the parser reaches a <script>
tag inside the <head>
, it stops to fetch and run it. Adding async or defer is highly recommended if your scripts are placed at the <header>
of the page. [18] [19] ❗
Reduce JavaScript Payloads with Code Splitting: A common anti-pattern is to load the whole bundle when a user lands on the website, even though the landing page doesn't need JS of all 30 other pages. In order to optimize the downloading times and bundle size of the application, use Code Splitting for your application. [20] [21] ⚠️
Modern web applications often use bundling tools like Webpack or Parcel. Whatever bundler you use, there are certain aspects to take care of:
Testing is an essential part of the application lifecycle and it increases the maturity of the codebase with increasing feature-set. Jest provides the most advanced test-runner with a lot of built-in assertions. react-testing-library
You should write e2e integration tests that can fire up some actions, trigger sagas to execute processes of application, and then finally test the expected state of reducers. ⚠️
This is the most crucial part of testing and if the project uses redux-sagas, the testing experience is really intuitive and meaningful. This is another reason to ditch redux-thunks in favor of sagas.
With simple test assertions of Jest, one should write unit tests for all utility functions/helpers present in the codebase to ensure correct behavior. ⚠️