React and React Native
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
dependenciesanddevDependencies. Thedependenciessection should include only the packages necessary to run the code, whiledevDependenciesshould 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. You can use npmjs.com search and npms.io to find and compare packages. Checking the number of stars can also help indicate popularity.
React and React Native general standards
Naming conventions
- Use PascalCase in components, interfaces, or type aliases e.g.
function TodoList(props: IProps) { ... }. - Interfaces should start with I e.g.
IAccount. - Inline prop interfaces should be
IProps. - Use camelCase for data types like constants, 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.tsandsrc/utils/dateUtils/utils.tsandsrc/components/todoList/TodoListModal.tsx - 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 ofvalueusehasValue. - 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
functionkeyword 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 keep 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
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, rather use an unique ID
- Use shorthand for boolean props e.g.
<Form hasValidation />instead of<Form hasValidation={true} /> - Avoid curly braces for string props e.g.
<Button title="Save" />instead of<Button title={"Save"} /> - Use template literals for longer string concatenations e.g.
const userDetails = ``${user.firstName} ${user.lastName}``; - Use object destructuring where possible e.g.:
const { firstName, lastName, email } = user;
return (
<>
<div> {firstName} </div>
<div> {lastName} </div>
<div> {email} </div>
</>
);
React
Project folder structure
- Following our custom
create-react-appfolder structure - Pages, components, utils etc.
UI framework
All UI at Bob Group that makes use of React should implement the shared UI framework found here. The framework is a custom set of React UI components used based on TailwindCSS and Headless UI.
App-wide state (in the context store)
App-wide state is stored in the app-wide context (instead of something more complex like Redux). This is done by creating context i.e. const StoreContext = createContext(); and passing it down through a provider e.g. <StoreContext.Provider value={this.state}>. It's then used by calling let store = useStore();.
Order of things
Please note: The main render() function is our convention, not React's convention. We find that it enhances readability by compartmentalising the main render function's variables and makes it easier to locate the main render function in a component.
// 1. Imports
import { useState, useEffect } from "react";
// 2. Additional variables (non-global)
const someConstant = "123";
// 3. Props interface (if applicable)
interface IProps {
someProperty: string;
anotherProperty: number:
setHasSomethingChanged: (val: boolean) => void;
onSubmit?: () => void;
}
// 4. Component
function AccountList(props: IProps) {
// Deconstruct non-function properties
const { someProperty, anotherProperty } = props;
const [isLoadingShipments, setIsLoadingShipments] = useState<boolean>(true);
const [isSaving, setIsSaving] = useState<boolean>(true);
useEffect(() => {
...
}, []);
function fetchAccounts() {
...
}
function onSave() {
...
}
/* --------------------------------*/
/* RENDER METHODS */
/* --------------------------------*/
function renderButton() {
return (
<Button.Primary>
onClick={() => {
props.setHasSomethingChanged(true)
props.onSubmit && props.onSubmit()
}}
title={anotherProperty}
/>
)
}
function render() {
return (
<div>
{Boolean(someProperty) && renderButton()}
</div>
);
}
return render();
}
// 5. Exports
export default AccountList;
Prettier
We use Prettier in all of our JavaScript/TypeScript projects. The standardised config we use:
{
"trailingComma": "none",
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"printWidth": 100,
"arrowParens": "avoid"
}
Ideally set up your IDE to format using Prettier on saving of the file.
TypeScript and its config
All new JS components should be written in TypeScript. TypeScript detects more syntax errors, supports optional static typing, new features with browser compatibility and an enhanced IDE experience (code navigation and better autocompletion). The IDE also flags errors, including type-related as soon as they occur.
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"]
}
React Native
For mobile app development, we're using Expo. Expo is a bundle of tools created around React Native to help you create and test a React Native app more efficiently. Expo also helps you with the distribution of your React Native apps.
To improve the code quality and efficiency we're using ESLint, Prettier, Husky, and lint-staged to automate formatting and consistency checks.
App-wide state (in the context store)
App-wide state is stored in the app-wide context (instead of something more complex like Redux). This is done by creating context e.g. const MyContext: any = createContext(defaultState); and passing it down through a provider e.g <MyContext.Provider value={appWideContext}>. It's then used by calling const context: any = useContext(MyContext);.
TypeScript
TypeScript is also used as is the case with our React apps, but we have a combination of tsconfig.json and .eslintrc. This can change slightly between projects, especially the import resolver and paths.
.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"
}