Coding standards - React and React Native
React and React Native are powerful, open source JavaScript frameworks for building user interfaces and mobile applications.
TypeScript
Wherever possible, we use TypeScript. Here are some reasons why we choose to use TypeScript:
- Strong typing: TypeScript adds static typing to JavaScript, allowing you to define types for variables, function parameters, and return values. This helps catch errors at compile-time, reducing the chances of runtime errors and enhancing code quality. It also improves code documentation and makes it easier for developers to understand and maintain the codebase.
- Early error detection: The static typing provided by TypeScript enables early detection of errors during development. The TypeScript compiler can catch common mistakes, such as type mismatches, missing properties, and incorrect method invocations, before the code is even executed. This helps in reducing bugs and enhances overall software reliability.
- IDE support and tooling: TypeScript has excellent support in modern Integrated Development Environments (IDEs) such as Visual Studio Code. With TypeScript, IDEs can provide features like autocompletion, intelligent code navigation, refactoring, and code documentation. The TypeScript compiler also provides rich error messages, allowing developers to quickly locate and fix issues.
- Scalability and maintainability: As projects grow in size and complexity, maintaining and evolving them becomes challenging. TypeScript's static typing helps in building more robust and scalable applications. It enables developers to catch potential issues early on, refactor code with confidence, and collaborate more effectively within larger teams.
Dependencies
- The use of Node.js and yarn is assumed for package management. All packages and dependencies required to build and run the code should be defined in the
package.json
. for the project. - Avoid relying on globally installed packages for running or building the code.
- When installing packages, it's important to differentiate between
dependencies
anddevDependencies
. Thedependencies
section should include only the packages necessary to run the code, whiledevDependencies
should be used for packages required for building, testing, or deploying the code. - Install new packages only when there is a valid reason. The addition of any third-party dependency should be a team decision.
- Regularly remove unused packages to keep the project clean and lightweight.
- It is recommended to choose popular packages. Websites like https://www.npmjs.com/search and https://npms.io can provide an analyzed ranking of packages, and you can also consider checking the number of stars as an indicator of popularity.
React
Naming conventions
- Use PascalCase in components, interfaces, or type aliases e.g.
const TodoList = () => { ... }
. Interfaces should start with I e.g.IAccount
. Inline prop interfaces should beIProps
- Use camelCase for data types like variables, arrays, objects and functions e.g.
const usersToDelete = []
- Use camelCase for folder and non-component file names and PascalCase for component file names e.g.
src/hooks/useForm.ts
andsrc/components/dateUtils/utils.ts
- When naming boolean variables, add an appropriate prefix to the verb or noun describing the variable to better indicate that the variable is a boolean. e.g. Instead of
loading
, useisLoading
. Instead ofvalue
usehasValue
- When naming state variables to show/hide modals, make use of
const [modalToShow, setModalToShow] = useState<string>("")
orconst [shouldShowXModal, setShouldShowXModal] = useState<boolean>(false)
Imports
- Prefer destructuring imports to optimise bundling e.g.
import { useState, useEffect } from "react";
- Do not import all from lodash (
import _ from "lodash";
), but ratherimport debounce from "lodash/debounce";
General conventions
- Prefer boolean truthy detection
Boolean(x)
over double!!
e.g.!!x
- Use
Boolean(x)
when rendering conditional components if the condition is not a boolean itself e.g.Boolean(x) &&
- Split functions (especially render functions) into smaller functions
- From now on, always use functional components and change class components to functional components as you come across them
- For every API call, create a reusable interface for the response object
- As always, make suggestions if you feel something can be improved
- Remove all console statements when done. Production code should ideally not contain any console statements
- Prefer the
function
keyword to define a function over using e.g.const update = () =>
- Where possible, don't compound state into one object e.g.
loadingState = {isLoadingAccount: false, isLoadingBalance: true}
. Split them into separate state variables. - Include a comment section for
/* RENDER METHODS */
, as shown in the order of things below. This splits the "View" from the "Controller" (functions, state, props etc.) - Have a main render() function, even for functional components, as shown in the order of things below. This keeps variables for the render functions scoped and neat
- Capitalise abbreviations e.g.
shipmentID
. Where there are two abbreviations next to each other, lowercase the first e.g.eftPOP
Order of things
// 1. Imports import { useState, useEffect } from "react"; // 2. Additional variables const SOME_CONSTANT = "123"; // 3. Props interface (if applicable) interface IProps { someProperty: string, onSubmit: Function } // 4. Component function AccountList(props: IProps) { // Deconstruct all non-function properties const { someProperty } = props; const [isLoadingShipments, setIsLoadingShipments] = useState<boolean>(true); const [isSaving, setIsSaving] = useState<boolean>(true); useEffect(() => { ... }, []); function fetchAccounts() { ... } function onSave() { ... } /* --------------------------------*/ /* RENDER METHODS */ /* --------------------------------*/ function renderButton() { ... } function render() { return ( <div> {props.onSubmit()} {someProperty} </div> ); } return render(); } // 5. Exports export default AccountList;
Project folder structure
- Following our custom
create-react-app
folder structure- Pages, components, utils etc.
UI framework
We've developed our own UI framework that is a custom set of React UI components based on TailwindCSS and Headless UI.
Input fields
- For search inputs, use the
<SearchInput />
component from the UI framework. It has no placeholder and includes a search icon. The label should be descriptive e.g. “Search waybill no”. - For
<Select />
components, avoid placeholder text unless specifically required. - For
<Input />
components, use placeholder text sparingly and only to provide examples e.g.John Doe
.
App-wide state (in the context store)
App-wide state is stored in the context rather than a complex solution like Redux. You create context using const StoreContext = createContext();
and pass it via a provider <StoreContext.Provider value={this.state}>
. It is accessed using let store = useStore();
.
Prettier
We use Prettier across all JS/TS projects. The standard config we use is:
{ "trailingComma": "none", "tabWidth": 2, "useTabs": false, "singleQuote": false, "printWidth": 100, "arrowParens": "avoid" }
Ideally, configure your IDE to auto-format using Prettier on save.
TypeScript and its config
All new JS components should be written in TypeScript. TypeScript detects more syntax errors, supports optional static typing, enables modern features with browser compatibility, and improves IDE experience (e.g. navigation, autocompletion).
Here is the tsconfig.json
for React projects:
{ "compilerOptions": { "types": ["google.maps"], "baseUrl": "src/", "target": "es5", "module": "esnext", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "allowSyntheticDefaultImports": true, "noFallthroughCasesInSwitch": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true }, "include": ["src"] }
Using any
and @ts-ignore
Use any
and @ts-ignore
only where absolutely necessary.
Other little things to notice
- Avoid using indexes as key props — use a unique ID instead.
- Use shorthand for boolean props e.g.
<Form hasValidation />
instead of<Form hasValidation= />
- Avoid curly braces for string props e.g.
<Button title="Save" />
instead of<Button title=Save />
- Use object destructuring where possible e.g.
const { firstName, lastName, email } = user; return ( <> <div> {firstName} </div> <div> {lastName} </div> <div> {email} </div> </> );
- Use template literals for string concatenation e.g.
const userDetails = `${user.firstName} ${user.lastName}`
React Native
For mobile app development, we're using Expo. Expo is a toolchain built around React Native to help you develop, build, and distribute apps more efficiently.
To improve code quality and consistency, we use ESLint, Prettier, Husky, and lint-staged to automate formatting and linting.
App-wide state (in the context store)
As with React, app-wide state is stored using the context API. This is done by creating context i.e. const MyContext: any = createContext(defaultState);
and passing it down through a provider e.g. <MyContext.Provider value={appWideContext}>...</MyContext.Provider>
. It's then used by calling const context: any = useContext(MyContext);
.
TypeScript
TypeScript is used in all React Native projects too. We use a combination of tsconfig.json
and .eslintrc
files, which may vary slightly depending on the project.
.eslintrc
{ "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json", "tsconfigRootDir": "./" }, "plugins": ["react", "@typescript-eslint", "prettier", "jest", "import"], "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:jest/recommended" ], "rules": { "no-console": "off", "no-useless-escape": "off", "@typescript-eslint/no-empty-function": "off", "react/prop-types": "off", "react/no-unescaped-entities": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-empty-interface": "off", "no-unused-vars": "off", "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": [ "error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" } ], "prettier/prettier": 2, "jest/no-disabled-tests": "warn", "jest/no-focused-tests": "error", "jest/no-identical-title": "error", "jest/prefer-to-have-length": "warn", "jest/valid-expect": "error" }, "settings": { "react": { "pragma": "React", "version": "detect" }, "import/resolver": { "alias": { "@/components/": ["./src/components/"], "@/styles/": ["./src/styles/"], "@/utils/": ["./src/utils/"], "@/data/": ["./src/data/"], "@/trades/": ["./src/components/screens/trades/"], "@/order/": ["./src/components/screens/order/"], "@/MyContext": ["./src/MyContext"], "@/DataStore": ["./src/DataStore"] } } }, "env": { "jest/globals": true } }
tsconfig.json
{ "compilerOptions": { "jsx": "react", "noImplicitAny": true, "moduleResolution": "node", "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noEmit": true, "baseUrl": ".", "outDir": "./build", "paths": { "@/components/*": ["src/components/*"], "@/styles/*": ["src/styles/*"], "@/utils/*": ["src/utils/*"], "@/data/*": ["src/data/*"], "@/trades/*": ["src/components/screens/trades/*"], "@/order/*": ["src/components/screens/order/*"], "@/MyContext": ["src/MyContext"], "@/DataStore": ["src/DataStore"] } }, "include": ["./src"], "extends": "expo/tsconfig.base" }