TickLab LogoTickLab

Set up Front-end development environment

Posted on Nov 18, 2023

This article is based on the experiences I gained while participating in a project at TickLab as a Front-end developer. The content includes setting up and configuring the user interface development environment on a web platform from the start of the project's implementation phase and effective teamwork among team members to ensure code quality. Readers can create issues on my GitHub repository if they have any questions or suggestions for the article.

1. Overview of the Tools Used

  • ReactJS is a JavaScript library used to build flexible and reusable user interfaces ( https://react.dev ).
  • TailwindCSS is a powerful CSS framework that allows for quick and easy interface building ( https://tailwindcss.com ).
  • Vite is a fast and efficient front-end build tool that speeds up the development process ( https://vitejs.dev ).
  • Zustand is a state management library for ReactJS, making application state management simple and easy ( https://zustand-demo.pmnd.rs ).
  • TypeScript is a popular programming language, an extended version of JavaScript with static type support, helping to reduce errors and improve development performance (https://www.typescriptlang.org ).
  • ESLint, Prettier, Lint-staged, Husky: A set of tools for checking and maintaining code quality during software development. These tools ensure that the code adheres to coding rules and standards, making it readable, maintainable, and collaborative.

2. Starting with a Simple Example

The goal of this article is to introduce how to set up the user interface development environment, not to delve into product implementation. Readers can complete this small project themselves based on the template we will build.

Suppose you have a requirement to build the interface of a web-based management application that displays all the projects that have been and are being worked on in a company. After analyzing the business, you have the following requirements:

  • The application consists of 3 pages: login page, page displaying an overview of all projects, and page displaying detailed information of each project.
  • The application must include 4 basic operations: create - view - edit - delete.
  • The application must be compatible with desktop and mobile screens.

Next, you need to create a development environment for the developers in the team. Here, I will only introduce how to install the necessary configurations and create a folder structure for Front-end development; the APIs from the Back-end will be mocked by simply returning a string without detailed implementation.

3. Installation and Configuration of the Development Environment

3.1. Installation

  • Since we are working with TypeScript, we need a runtime environment and a package manager. I choose the two most popular tools: Node.js and Yarn. Go to the Node.js homepage (https://nodejs.org ) to install Node; a package manager npm is also installed with Node. You can use npm instead of Yarn, but Yarn uses cache more efficiently to store downloaded packages, saving time and bandwidth when reinstalling. Install Yarn using the command line:
    npm install -g yarn
  • Check the installed tools: 
    mediaBlockInlineBlock
  • Next, we need a base template from Vite, using the following command line:
    yarn create vite react-template --template react-ts
    We will have a react-template directory; delete the files: App.css, .eslintrc.cjs. The folder structure is as follows: 
    mediaBlockInlineBlock

3.2. Configuration

After each configuration step, you can check the folder structure corresponding to each commit on my GitHub .

  1. Configuring the CSS Framework

We use TailwindCSS for the project. Tailwind is designed with a utility-first approach, including a set of pre-defined class elements that make front-end development fast and flexible. Using Tailwind skillfully, you might not even need to write any pure CSS files in the project. The installation steps are as follows:

  • Install tailwindcss via Yarn and create a tailwind.config.js file (note that tailwindcss should be installed in devDependencies because the Tailwind class elements will be generated into static CSS files before running in the browser):
    1yarn add -D tailwindcss
    2yarn tailwindcss init
  • Add the paths to the files using Tailwind in tailwind.config.js:
    1/** @type {import('tailwindcss').Config} */
    2export default {
    3 content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
    4 theme: {
    5 extend: {},
    6 },
    7 plugins: [],
    8}
  • Add the @tailwind directives for each Tailwind layer into the index.css file to activate and use the pre-defined CSS classes in the Tailwind CSS framework. Specifically, there are three main layers that need the @tailwind directive, refer here. Note to delete all old content in the index.css file and just add the three Tailwind directives:
    1@tailwind base;
    2@tailwind components;
    3@tailwind utilities;
  • Try generating the static CSS file from the three directives in index.css:
    1yarn tailwindcss -i ./src/index.css -o ./dist/output.css
  • We will see a dist folder appear containing the output.css file, which is a static CSS file generated from Tailwind. Import dist/output.css into the App.tsx file, and also delete all old content and replace it with the following simple code:
    1import '../dist/output.css';
    2function App() {
    3 return (
    4 <h1 className="text-3xl font-bold underline">
    5 Hello world!
    6 </h1>
    7 );
    8}
    9
    10export default App;

    Execute the command line yarn dev, by default Yarn uses port 5173 to host our website locally, open the web browser at localhost:5173 and here is the result:

    We notice that the process from writing Tailwind CSS classes to rendering the UI is quite manual: we have to generate the static CSS file and then import it into each code file that uses Tailwind CSS. If our project has about 50 .tsx code files using Tailwind CSS, we have to import 50 times the line import '../dist/output.css', but not all files use all the generated classes, which slows down the page load and creates unnecessary CSS code. Additionally, the CSS code generated by Tailwind CSS may not be compatible with some older browsers like Safari, Internet Explorer, etc. Therefore, we need to use CSS processors to automate and optimize this process; two CSS processors used are postcss and autoprefixer. Install them in devDependencies:
    1yarn add -D postcss autoprefixer
  • Add the file postcss.config.js at the root folder with the following content:
    1export default {
    2 plugins: {
    3 tailwindcss: {},
    4 autoprefixer: {},
    5 },
    6}

    We have configured tailwindcss and autoprefixer as plugins for postcss. Add the class text-red-500 to the Hello world! code above, run the command yarn dev again to see the result:

    Even we don't need to run the command yarn tailwindcss -i ./src/index.css -o ./dist/output.css and import '../dist/output.css' in each file using Tailwind CSS; Vite will automate this process based on the postcss.config.js file. Delete the dist folder, and now just Ctrl+S after each update of Tailwind CSS classes.
  1. Configuring Code Quality Toolset (ESLint, Prettier, Lint-staged, Husky)

In the same project, team members may have their own coding styles. Using this toolset ensures that everyone writes code according to the same rules, from code formatting to coding conventions. This makes the code more readable and reduces the risk of simple mistakes.

a. Prettier

  • Is an automatic code formatting tool that standardizes code syntax. Prettier ensures that the code has the same structure, spacing, and syntax, creating beautiful and readable code, refer here. Install prettier in devDependencies:
    1yarn add -D --exact prettier
  • Create a .prettierrc file to configure Prettier with basic rules as follows:
    1{
    2 "printWidth": 100, // ensure that lines of code are no longer than 100 characters.
    3 "singleQuote": true, // require Prettier to use single quotes (') instead of double quotes (") to wrap around strings.
    4 "jsxSingleQuote": true, // similar to the above, but applicable to JSX (JavaScript XML) as well.
    5 "semi": true, // require Prettier to add semicolons (;) at the end of each line of code.
    6 "tabWidth": 2, // specify that Prettier will use 2 spaces for each tab.
    7 "bracketSpacing": true, // add a space after the opening curly brace '{' and before the closing curly brace '}'.
    8 "arrowParens": "always",// require Prettier to always place parentheses around the parameter of an arrow function.
    9 "trailingComma": "none" // specify that Prettier won't add a trailing comma in multi-line lists like arrays or objects.
    10}
  • Create a .prettierignore file so that Prettier knows which files do not need formatting:
    1dist
    2node_modules
    3package*.json
    4*.lock
    5*.yml
    6*.md
  • Format the App.tsx file using the command line yarn prettier ./src/App.tsx --write or add a script command for the entire project in package.json "format": "prettier '**/*.{tsx,ts,js,json,md,yml,yaml}' --write".

b. ESLint

  • Is a tool for checking code errors and coding rules in JavaScript and TypeScript, helping to detect and fix syntax errors, logic errors, and code conventions in the code, refer here. Install eslint along with eslint-config-prettier (to disable unnecessary or conflicting rules with Prettier) and a plugin of ESLint eslint-plugin-react-hooks (used when using React hooks and needing to follow the Rules of Hooks) in devDependencies:
    1yarn add -D eslint eslint-config-prettier eslint-plugin-react-hooks
  • Create a .eslintrc file to configure ESLint, adding basic rules, environments, and plugins as follows:
    1{
    2 "parser": "@typescript-eslint/parser", // specify that the TypeScript ESLint parser should be used to analyze TypeScript code.
    3 "extends": [
    4 "prettier", // integrate Prettier with ESLint for code formatting.
    5 "plugin:@typescript-eslint/recommended", // provide recommended rules for TypeScript code from the @typescript-eslint plugin.
    6 "plugin:react-hooks/recommended" // provide recommended rules for using React hooks.
    7 ],
    8 "plugins": ["@typescript-eslint"], // configuration uses the @typescript-eslint plugin.
    9 "parserOptions": {
    10 "ecmaVersion": 2018, // specify that the code is written in ECMAScript 2018 (ES9).
    11 "sourceType": "module", // indicate that the code is using modules (ES6 modules).
    12 "project": "./tsconfig.json" // specify the TypeScript project configuration file to use.
    13 },
    14 "env": {
    15 "node": true, // indicate that the code will run in a Node.js environment.
    16 "es6": true // indicate that ES6 (ECMAScript 2015) features are supported.
    17 },
    18 "rules": {
    19 "@typescript-eslint/no-inferrable-types": 0, // turn off the rule that flags unnecessary type declarations in TypeScript.
    20 "@typescript-eslint/no-unused-vars": 2, // set the rule that enforces no-unused-vars to an error level.
    21 "@typescript-eslint/no-var-requires": 0, // turn off the rule that disallows the use of require in TypeScript files.
    22 "eqeqeq": "error" // enforce strict equality (===) over loose equality (==) and sets it to an error level.
    23 }
    24}

    Since the project configuration in parserOptions is in the tsconfig.json file, we make some changes to this file to be compatible with ESLint as follows:
    • Remove Linting in compilerOptions since we have ESLint.
    • Change the file paths in include to specify that the TypeScript Compiler only needs to compile the following files:
      1"include": ["src", "**/*.ts", "**/*.tsx", "**/*.js", "*.cjs"]
    • Change the names of the *.config.js files (tailwind.config.js, postcss.config.js) to *.config.cjs (tailwind.config.cjs, postcss.config.cjs) to match the file paths in the include section (which means changing the syntax from ESModule to CommonJS).
    • Some other changes have been made in compilerOptions but are not detailed here; you can check my commit on GitHub.
    • Create a .eslintignore file so that ESLint knows which files do not need to be checked:
      1dist
      2node_modules
  • Run ESLint on the App.tsx file using the command line yarn eslint ./src/App.tsx --fix or add a script command for the entire project in package.json "lint": "eslint '**/*.{tsx,ts,js}' --fix".

c. Lint-staged

  • The two tools above may be sufficient for us to format code, check syntax, and check code conventions, but there is a drawback. They both run on the entire source code even if we only change one file, which wastes time and machine resources. Therefore, we use another tool called lint-staged, which limits the scope of Prettier and ESLint to only the files in the staging area of Git (files that have been git add), refer here. Install lint-staged in devDependencies:
    1yarn add -D lint-staged
  • Create a .lintstagedrc.json file to limit the scope of Prettier and ESLint:
    1{
    2 "**/*.{ts,js,tsx}": ["eslint --fix"],
    3 "**/*.{tsx,ts,js,json,md,yml,yaml}": ["prettier --write"]
    4}
  • Before running lint-staged, we must stage the changed files in Git, and the result is as follows:

d. Husky

  • With lint-staged, we can combine running Prettier and ESLint with a single command line yarn lint-staged, but there is still one issue: What if a team member forgets to run yarn lint-staged before committing code? This could break the code convention that the team has established, making the code messy and slowing down product development. Therefore, Husky comes as a solution to this problem. Husky is a tool that triggers actions set in Git hooks, refer here. We will use Husky to configure the pre-commit Git hook for lint-staged, ensuring that every time team members commit code, the command yarn lint-staged is automatically executed. Install husky in devDependencies and install the Git hook:
    1yarn add -D husky
    2yarn husky install
  • You will see a .husky directory appear containing a file named husky.sh, which includes shell script code that Husky uses to perform actions when Git hooks are triggered.
  • Create a pre-commit Git hook to trigger the command yarn lint-staged each time before committing using the following command line:
    1yarn husky add .husky/pre-commit 'yarn lint-staged'

    If there are any errors violating the rules of Prettier and ESLint, the commit will be blocked, ensuring that only code that follows the rules is pushed to the common repository.
  • After committing and pushing to GitHub, only the pre-commit file is pushed up; the husky.sh file has been ignored by Git. Therefore, we will add a script command "prepare": "husky install" in package.json to automatically initialize husky.sh when running yarn install, without the need for team members to set it up again when pulling the code.

Now, the developers in the team just need to code and commit; everything related to coding conventions and syntax error checking has been automated by the toolset ESLint, Prettier, Lint-staged, and Husky.

  1. Configuring a UI Library Combining ReactJS and TailwindCSS

The reason we have this step is that it is difficult for developers to remember and use a huge system of class elements in TailwindCSS; we need a wrapper library built on TailwindCSS and supporting ReactJS. I choose Material Tailwind for this project, refer to https://www.material-tailwind.com/. Install material-tailwind in dependencies:

1yarn add @material-tailwind/react

4. Organizing Source Code

For a project, organizing the source code clearly and explicitly is extremely important, especially when the project size starts to grow and more personnel join in. It must optimize the reading comprehension process of the developers in the team and minimize code duplication. Below is a “pattern” that the Engineer team at TickLab used during product development, illustrated through the example in section 2.

  • We start with the first requirement: The application consists of a login page, a page displaying an overview of all projects, and a page displaying detailed information of each project. Therefore, we need a src/pages directory containing all the main UI pages. Create 3 pages in the pages directory:
    • AuthPage: login page.
    • ProjectGeneralPage: page displaying an overview of all projects.
    • ProjectDetailPage: page displaying detailed information of each project.

Each page will be divided into components such as button, table, navigation bar, search bar, pagination bar, etc. These components can be reused multiple times within a page or across pages. A src/components directory is responsible for storing reusable components.

Depending on the UI/UX design of each project, but usually, the layout of the login page will be different from the layout of the other pages. Therefore, we will create a src/layouts directory including 2 main layouts: AuthLayout.tsx and AppLayout.tsx.

We create two more directories src/interfaces and src/types to create custom object structures and data types for the project, and install the package @types/node to support data types in Node.js.

  • Next, with the second requirement: The application must include 4 basic operations: create - view - edit - delete. This requirement demands that the application interact with APIs from the back-end. We will create a directory named src/services to hold the API calls. At this step, we need to note a few things:
    • Put the server's URL into a file named .env. Because in a real project, we will work with different servers depending on the development stage, for example, http://localhost:8080 in the Development environment, https://staging.example.com in the Staging environment, andhttps://api.example.com in the Production environment. Just change the server URL in the .env file without modifying the source code.
    • Put .env in .gitignore, and also create a .env.example file to provide a template for .env and allow pushing this file to the common repository. This helps team members recognize the environment variables used in the project and set them up appropriately.

Usually, the code segments for API calls have similar logic. Therefore, we write a function invoke to handle this logic and place it in the file services/common.ts. I will mock a simple back-end to fake Authentication and CRUD APIs; the logic is placed in the __mock_server__ directory (remove this directory when working with real APIs). You can see the implementation of the invoke function and how it is used in authService and userService in my commit on GitHub.

We can end the folder setup at this step for projects with small workloads, only solving simple business tasks like registration, login, CRUD, etc. There will be an issue when the workload increases and especially when more personnel join your project, which is managing global states. If you only manage states using the useState hook in React and the Passing Props method (passing state from parent component to child component), then besides you, no one else can join the project, or it will take a lot of time for other team members to read and understand the source code. This also affects the scalability of the project when developing new features, and the worst consequence could be having to "tear down and rebuild," slowing down the product release time for customers. Therefore, the global state issue must be addressed first right at the development environment setup step. A relatively common solution in most projects is to use a State Management Library; popular tools include Redux, Zustand. Here, I use Zustand because its architecture is simpler than Redux, using React Hooks to manage independent stores, making it easier to access state and actions, refer here. Install zustand in dependencies:
yarn add zustand
Create a src/states directory to store the stores. For data returned from APIs, we build an API call flow as follows:

The above rule ensures that team members understand the execution flow of the code, increasing readability and reducing bug fixing time. View the back-end as providing interfaces to create types for the data returned from APIs. Call APIs in services, using the created types. Create a store to store states and actions for a set of APIs related to a product feature (Authentication, CRUD, Automatic Notification, etc.). Use these stores in components and pages. I will implement 2 stores: useUserStore and useProjectGeneralStore including state and actions of the CRUD API set. You can see the details in my commit on GitHub.

  • The final requirement: The application must be compatible with desktop and mobile screens. This is the responsive feature of the product. The design principle in UI/UX design for web applications is mobile first, meaning each module in the design must be compatible on mobile first, then scaled up to larger screens like tablets, desktops, etc. TailwindCSS also follows this design principle with a breakpoint system including 5 basic breakpoints: xssmmdlgxl. However, for components that change significantly when switching between screen sizes, using breakpoints can be more difficult than creating new "versions." Therefore, we will create a hook named useScreenSize responsible for tracking changes in screen size (implement this hook in the src/hooks directory - this directory contains all the custom hooks we define) and an enum ScreenSize containing screen types in the src/constants directory (all constants in the project are placed in this directory) to support switching between component versions when resizing. You can see the detailed implementation of useScreenSize and how this hook is used in ProjectGeneralPage.tsx in my commit.

Because hooks can only be called inside function components and other custom hooks, we will create an additional src/utils directory to contain functions that handle normal tasks.

We have gone through the 3 most common requirements of most Management Applications to build a folder structure for the Front-end developers team. Now, let's look back at the entire source code and see that: Instead of using complex paths, having to import each module individually even though they belong to the same directory, we will use path alias in TypeScript and coordinate with a tool that automatically "packages" the paths into an index.ts file in each directory, which is barrelsby (refer here). Install barrelsby in devDependencies:

1yarn add -D barrelsby

Create a .barrelsby.json file to specify the directories that need packaging:

1{
2 "directory": [
3 "./src/components",
4 "./src/constants",
5 "./src/hooks",
6 "./src/interfaces",
7 "./src/layouts",
8 "./src/pages",
9 "./src/services",
10 "./src/states",
11 "./src/utils"
12 ],
13 "delete": true
14 }

Create a script command "barrels": "barrelsby --config .barrelsby.json -q" and execute it with the command line yarn barrels; the index.ts file will automatically appear in the directories specified by .barrelsby.json. Additionally, configure paths and resolve alias in tsconfig.json and vite.config.ts (specifically in commit), after that, we can use path aliases to simplify import operations.

5. Conclusion

I will end part 1 of this article here; all setup steps are saved in corresponding commits on the GitHub repository. In part 2, I will share with you how to set up a container environment to deploy the web application to a virtual machine (Virtual Private Server) using Docker Container and Nginx Web Server based on this template. This is a preliminary step to create CI/CD workflows for the project.

6. References

  1. Responsive Design: https://tailwindcss.com/docs/responsive-design
  2. Sensor management web application: https://github.com/HPCMonitoring/sensor-manager