Base code for building ReactJS apps. Uses typescript and was built on top of create-react-app.
Uses the following tech stack:
-
React v17.0.2 - main rendering library
-
Ant Design - component library
-
mobx - state management
-
axios - http client for sending server requests
-
i18n-js - language translation library
-
prettier - code formatter
-
jest - general testing framework for javascript
-
enzyme - testing framework specifically for React Components
-
Node v16.17.0 or newer
-
NPM (packaged with node) v8.19.1 or newer
-
Python v3.10.6 or newer
-
Yarn v1.22.19 or newer
-
Visual Studio Code - for code editing (or any IDE preferred)
When using VS code, these extensions are highliy encouraged.
-
ES Lint by Microsoft
$ yarn install # install dependencies
$ yarn start # start the application$ yarn run build # Creates a build folder which can be used by the server$ yarn test # Runs all the tests created and shows resultsShould be strictly followed unless there is an important reason not to.
- Never use class based components except for ErrorBoundary class.
- Always use hooks.
- Use async and await instead of promises to prevent then-catch chain hell (except for using axios in API calls).
- Always use interfaces for props definition for each component.
- Use enums wherever appropriate.
- Use color constants for hex colors (/src/theme/color.ts).
- For multiple concurrent async functions, use Promise.all.
- Follow setup-exercise-verify-teardown principle for testing.
- Always resolve errors or warnings identified by the linter. Pull requests with errors or warnings will not be merged. Do not use ignore warning lines as well.
- Always split the code into multiple smaller functions. If you have a very long code, break it down to smaller parts especially those blocks that can be reused.
- Always provide a method signature for each function.
- Commenting on the logic of the code is highly encouraged.
- Create multiple small files instead of writing a very big file.
- Always use the appropriate names for functions, files, and folders. Follow the format of existing ones for reference.
- Always write tests for your own code. Do not let others do it for you.
- Always use the DRY (do not repeat yourself) principle.
- Never place in line css code inside components.
- Always destructure your props to make the code cleaner.
- Use the mobx store for properties that need to be stored in local storage. For other data, use the
useStatehook instead. - Use the
useMemohook for expensive operations. - Always use typescript (
.tsxand.tsfiles) unless there is an important reason not to. - Always use the optional chaining operator if a property can be null or undefined.
- Do not use
console.logever. For logging, use thelogfunction in the/src/config/consolefile. Note: this will only log in dev mode. - Create an
index.tswithin each folder for exporting. This will reduce repeating names on the imports. - The
src/componentsfolder are for reusable components only. Screen specific components are to be placed in their own screen folders. - For spacing, always use the spacing values found in the
themedirectory. - Always use types whenever possible.
-
Always use
yarn. Why yarn? It is faster and more secure at the cost of space.$ yarn add <package-name>
-
Do not add packages randomly. Ask permission first. There should be a solid reason or rationale for doing so.
-
Clearly identify if the package to be installed is used only in development and not in production. In this case, save it only as a dev dependency.
$ yarn add -D <package-name> # for dev only modules $ yarn add <package-name> # for non-dev modules
-
All environment variables are to be added in the
.envfile. -
All variable names should start with the
REACT_APP_prefix and should be in the upper case. For example, we want to addAPI_KEY. The correct variable name should beREACT_APP_TIMEOUT_DEFAULT. For more info, there is a documentation about adding custom ENV variables and why the use of the REACT_APP prefix. -
All newly added variables should also be added in the
.env.templatefile. The.envfile is ignored by git, creating a new one is required for new installs. The.env.templatefile will serve as basis for the required environment variables. -
Do not store any secrets (such as private API keys).
-
When using environment variables, place all of it in the properties file. To access the variable, use the
process.env.<VARIABLE_NAME>syntax. As an example, use theprocess.env.REACT_APP_TIMEOUT_DEFAULTwhen creating the variable mentioned in item 2. -
The environment variables are embedded during build time. This means that the values are only read during the
yarn startcommand. To reflect the new changes of the.envfile, the app should be stopped and restarted.
-
Create a separate folder in the
src/componentsdirectory (e.g. button). -
Create separate files for the actual component, css styling, properties, and test.
-
The component file should contain code pertaining to the actual component itself. It should be named
<component-name>.tsxsince it will contain jsx code. -
The css styling file should contain all the css styles (in JSON format) related to the component. It should be named
<component-name>.css.ts.export const buttonStyle = { margin: 5, };
-
The props file should contain the prop definition of the component as an
interface./** * Prop types describing the required and optional props for the buttons. */ export interface ButtonProps { /** * The expected method to be called when the button is clicked. */ onClick: () => void; /** * Text that should be shown in side the button */ text: string; }
-
The test file should be named
<component-name>.test.tsx. -
Styling components using either css via the
classNameor directly injecting styles are allowed. ForclassNames, set it in theApp.cssfile in thesrc/themefolder. -
After finishing the component, export it in the
index.tsfile of thesrc/componentsdirectory.
-
Create a folder in the
src/screensdirectory using the page screen as name. -
Use the format
<screen-name>.tsxas file name. -
Create a test file as well using the
<screen-name>.test.tsxname format. -
Add prop and css files whenever appropriate.
-
For texts, always use the
translatefunction found in thesrc/i18n/translate.tsfile. -
The
translatefunction will check the locale of the browser (default is English which is represented by theen.jsoninside thelocalefolder). The first level props represents the screen. The second level props represent the name of the text. For example, if we are going to access thegreettext from thehelloWorldpage, the text can be retrieved by invokingtranslate("helloWorld.greet"). Example below.import { translate } from "./translate"; export const HelloWorld = <h1>translate("helloWorld.greet")</h1>;
-
Use the mobx store only if the data needs to persist or be stored in
localStorageor the values are needed by other components that cant be retrieved using the parent child relationship. -
When creating a model store, always use a flat design (unless there is a specific reason not to).
-
Appropriately use the correct data type for each property.
-
Follow steps below to create a model store:
-
Create a folder named
<store-name>-store. -
Inside the folder, create a file named
<stora-name>-store.ts. -
Inside the store file, follow the format below. The example is we are making a count model that stores a count number value.
import { Instance, SnapshotOut, types } from "mobx-state-tree"; export const CountStoreModel = types .model("Count") .props({ count: types.number, }) .views((self) => ({ getCount: () => { return self.count; }, getFormattedCount: () => { return `Count: ${self.count}`; }, })) .actions((self) => ({ increment: () => { self.count = self.count + 1; }, decrement: () => { self.count = self.count - 1; }, })); const DEFAULT_STATE = { count: 0, }; type CountType = Instance<typeof CountStoreModel>; export interface Count extends CountType {} type CountSnapshotType = SnapshotOut<typeof CountStoreModel>; export interface CountSnapshot extends CountSnapshotType {} export const createCountDefaultModel = () => types.optional(CountStoreModel, DEFAULT_STATE); export const getDefaultCountStoreModel = () => DEFAULT_STATE;
-
The methods inside
viewslet us get the data from the store. In the example above, if the value of count is 5, the return of the getCount method would also be 5. Not only do we return the raw value of the property, we can also transform it to whatever we want. See example forgetFormattedCountfunction.// This will result to 5 const count = countStore.getCount(); // Will result to `Count: 5` const formattedCount = countStore.getFormattedCount();
-
For updating the values inside the store, define functions inside
actions. In the example above, theincrementfunction increases the count to 1 while thedecrementfunction reduces it by 1. -
Always define a
default state. The default state will be used during first load of the application especially if the localStorage have no record of the state. -
After defining the model, register it in the
src/models/root-store/root-storefile like the example below. Be mindful in the naming of the store. For example if you want to name the store as this will be used by all the components that needs access to the data.import { Instance, SnapshotOut, types } from "mobx-state-tree"; import { CountStoreModel, getDefaultCountStoreModel, } from "../count-store/count-store"; /** * A RootStore model. */ export const RootStoreModel = types.model("RootStore").props({ countStore: types.optional( CountStoreModel, getDefaultCountStoreModel() ), });
-
Export the contents of the file inside the
index.tsfile of thesrc/modelsdirectory.export * from "./count-store/count-store";
-
Create a test file for the contents of the store. See example below for the CountStore.
import { createCountDefaultModel } from "./count-store"; describe("CountStoreModel", () => { it("should correctly increase and decrease count.", () => { // setup const snapshot = createCountDefaultModel(); const incrementStore = snapshot.create(); const decrementStore = snapshot.create(); // exercise incrementStore.increment(); decrementStore.decrement(); // verify expect(incrementStore.count).toBe(1); expect(decrementStore.count).toBe(-1); }); });
-
-
To use the store data inside a component, use the
observermethod from themobx-react-litelibrary and use theuseStoreshook inside thesrc/models/root-store-context.tsfile. The observer function will trigger a check for updates whenever the store values being listened to are updated.import { observer } from "mobx-react-lite"; import { useStores } from "../../models"; export const HelloWorld = observer(() => { const { countStore } = useStores(); const count = countStore.getCount(); return <h1>{count}</h1>; });
-
For API calls, put these inside the
actionspart of the store. Then update the value of the model whenever a result is received from the request. In the example below, assume we have an API service calling the updated count saved in the server.import { Instance, SnapshotOut, types } from "mobx-state-tree"; import { getCount } from "./countApi"; export const CountStoreModel = types .model("Count") .props({ count: types.number, }) .actions((self) => ({ updateCount: (count: number) => { self.count = count; }, })) .actions((self) => ({ getCountFromServer: async () => { const countResult = await getCount(); self.updateCount(countResult); }, }));
-
Use the
axioslibrary for API calls. -
Use promise then-catch format.
import axiosInstance from "./axios-instance"; import { log } from "../../config"; export const login = async (username: string, password: string) => { return await axiosInstance .post("http://test/com", { username, password }) .then((result) => { return result.data; }) .catch((error) => { log("Unable to login: ", error); return null; }); };
-
Always log the error returned from the server. The return value should be based on the logic of the API call.
-
For each API endpoint, use only 1 axios instance. Avoid using a new instance of axios every time a request is made. Create a separate
axios-instance.tsfile and import its content. See example below.// Do not use this for your API calls. // This creates a new instance of axios every time import axios from "axios"; // Inside axios-instance.ts import axios, { AxiosInstance } from "axios"; import { API_KEY, API_URL } from "../../config/properties"; export const axiosInstance: AxiosInstance = axios.create({ baseURL: API_URL, timeout: 5000, headers: { "x-api-secret": API_KEY, }, }); export const setToken = (token: string) => { if (token) { axiosInstance.defaults.headers.common.authorization = `Bearer ${token}`; } else { delete axiosInstance.defaults.headers.common.authorization; } }; // When using an API see step 2 on how to use the axios instance.
-
For attaching intercepting requests and responses, use axios intereceptors.
-
Alway use the setup, exercise, verify, and teardown format.
-
Use jest for general testing (functions, models, API calls, etc.).
-
Use enzyme exclusively to test components.
-
Test coverage should be 100%.
- Modern Javascript Tutorial ⭐
- React JS Official Documentation ⭐
- What is REST API?
- Four-Phase Testing Pattern
- Jest Tutorial
- How to Test Using Jest and Enzyme
- Typescript Tutorial⭐
- What is React JS?
- ReactJS Tutorial ⭐
- Jest Crash Course
- Test Driven Development With Jest and Enzyme
⭐ = Highly Recommended
- When performing a commit using git,
prettieris invoked to format the code so that it will have a standardized style. This may cause a slight delay but wil not take too much time.