React Best Practices Handbook - Part II

Tuan Tran VanTuan Tran Van
38 min read

Let's explore React's best practices more.

React state must be immutable

Have you ever wondered why React makes such a fuss about immutability? As a newbie, you might think that Javascript’s mutation are perfectly fine. After all, we add or remove properties from objects and manipulate arrays with ease.

But here is the twist: in React, immutability isn’t about never changing state, it’s about ensuring consistency.

When you mutate state directly, React can’t detect changes reliably. This means your UI might not update as expected. The trick? replace old data with new copies.

For instance, if you need to add a user, you should create a new array with a new user included, rather than directly pushing a new user to an existing array.

const updatedUsers = [...users, newUser];

The code const updatedUsers = [...users, newUser]; uses the spread operator to create a new array, updatedUsers, which combines the existing users with newUser.

This approach maintains immutability in React by not modifying the original users array. Instead, it creates a new state representation, allowing React to optimize rendering and ensure predictable state changes. When you update the state using setUsers(updatedUsers);, React re-renders the component based on this new array, adhering to best practices for state management.

This ensures React detects the change and re-renders your component smoothly.

Clear Flow of execution

Having a clear flow of execution is essential for writing clean code because it makes the code easier to read, understand, and maintain. Code that follows a clear and logical structure is less prone to errors, easier to modify and extend, and more efficient in terms of time and resources.

On the other hand, spaghetti is a term used to describe code that is convoluted and difficult to follow, often characterized by long, tangled, and unorganized code blocks. Spaghetti can be the result of poor design decisions, excessive coupling, or lack of proper documentation and commenting.

Here are two examples of javascript code that perform the same task, one with a clear flow of execution, and the other with spaghetti code.

// Example 1: Clear flow of execution
function calculateDiscount(price, discountPercentage) {
  const discountAmount = price * (discountPercentage / 100);
  const discountedPrice = price - discountAmount;
  return discountedPrice;
}

const originalPrice = 100;
const discountPercentage = 20;
const finalPrice = calculateDiscount(originalPrice, discountPercentage);

console.log(finalPrice);

// Example 2: Spaghetti code
const originalPrice = 100;
const discountPercentage = 20;

let discountedPrice;
let discountAmount;
if (originalPrice && discountPercentage) {
  discountAmount = originalPrice * (discountPercentage / 100);
  discountedPrice = originalPrice - discountAmount;
}

if (discountedPrice) {
  console.log(discountedPrice);
}

As we can see, example 1 follows a clear and logical structure, with a function that takes in the necessary parameters and returns the calculated result. On the other hand, example 2 is much more convoluted, with variables declared outside of any function and multiple statements used to check if the code block has been executed successfully.

Reusability

Code reusability is a fundamental concept in software engineering that refers to the ability of code to be used multiple times without modification.

Code reusability is important because it can greatly improve the efficiency and productivity of software development by reducing the amount of code that needs to be written and tested.

By reusing existing code, developers can save time and effort, improve code quality and consistency, and minimize the risk of introducing bugs and errors. Reusable also allows for more modular and scalable software architectures, making it easier to maintain and update codebases over time.

// Example 1: No re-usability
function calculateCircleArea(radius) {
  const PI = 3.14;
  return PI * radius * radius;
}

function calculateRectangleArea(length, width) {
  return length * width;
}

function calculateTriangleArea(base, height) {
  return (base * height) / 2;
}

const circleArea = calculateCircleArea(5);
const rectangleArea = calculateRectangleArea(4, 6);
const triangleArea = calculateTriangleArea(3, 7);

console.log(circleArea, rectangleArea, triangleArea);

This example defines three functions that calculate the area of the circle, rectangle, and triangle, respectively. Each function performs a specific task, but none of them are reused for other similar tasks.

The code is inefficient since it repeats the same logic multiple times.

// Reusable function for calculating area of geometric shapes
function calculateArea(shape, ...args) {
  switch (shape) {
    case 'circle':
      const [radius] = args;
      const PI = 3.14;
      return PI * radius * radius;
    case 'rectangle':
      const [length, width] = args;
      return length * width;
    case 'triangle':
      const [base, height] = args;
      return (base * height) / 2;
    default:
      throw new Error(`Shape "${shape}" not supported.`);
  }
}

// Example usage
const circleArea = calculateArea('circle', 5); // Calculate area of circle with radius 5
const rectangleArea = calculateArea('rectangle', 4, 6); // Calculate area of rectangle with length 4 and width 6
const triangleArea = calculateArea('triangle', 3, 7); // Calculate area of triangle with base 3 and height 7

console.log('Circle Area:', circleArea);
console.log('Rectangle Area:', rectangleArea);
console.log('Triangle Area:', triangleArea);

This example defines a single function calculateArea that takes an shape argument and a variable number of arguments. Based on the shape argument, the function performs the appropriate calculation and returns the result.

This approach is much more efficient since it eliminates the need to repeat code for similar tasks. It is also more flexible and extensible, as additional shapes can easily be added in the future.

Conciseness vs Clarity

When it comes to writing clean code, it's important to strike a balance between conciseness and clarity. While it's important to keep code concise to improve its readability and maintainability, it's equally important to ensure that the code is clear and easier to understand. Writing overly concise code can lead to confusion and errors, and can make the code difficult to work with other developers.

Here are two examples that demonstrate the importance of conciseness and clarity.

// Example 1: Concise function
const countVowels = s => (s.match(/[aeiou]/gi) || []).length;
console.log(countVowels("hello world"));

This example uses a concise arrow function and regex to count the number of vowels in a given string. When the code is very short and easy to write, it may not immediately clear to other developers how the regex pattern works, especially if they are not familiar with regex syntax.

// Example 2: More verbose and clearer function
function countVowels(s) {
  const vowelRegex = /[aeiou]/gi;
  const matches = s.match(vowelRegex) || [];
  return matches.length;
}

console.log(countVowels("hello world"));

The example uses the traditional function and regex to count the number of vowels in a given string but does so in a way that is clear and easy to understand. The function name and the variable name are descriptive, and the regex pattern is stored in a variable with a clear name. This makes it easy to see what the function is doing and how it works.

It's important to strike a balance between conciseness and clarity when writing code. While concise code can improve readability and maintainability, it's important to ensure that the code is still clear and easy to understand for other developers who may be working with the codebase in the future.

By using descriptive functions and variable names and making use of clear and readable code formatting and comments, it's possible to write clean and concise code that is easy to understand and work with.

Single responsibility principle

The single responsibility principle (SRP) is a principle in software development that states that each class or module should have only one reason to change, in other words, each entity in our codebase should have only one responsibility.

This principle helps to create code that is easy to understand, maintain, and extend.

By applying SRP, we can create code that is easier to test, reuse, and refactor, since each module only handles a single responsibility. This makes it less likely to have side effects or dependencies that make the code harder to work with.

// Example 1: Withouth SRP
function processOrder(order) {
  // validate order
  if (order.items.length === 0) {
    console.log("Error: Order has no items");
    return;
  }

  // calculate total
  let total = 0;
  order.items.forEach(item => {
    total += item.price * item.quantity;
  });

  // apply discounts
  if (order.customer === "vip") {
    total *= 0.9;
  }

  // save order
  const db = new Database();
  db.connect();
  db.saveOrder(order, total);
}

In this example, the processOrder function handles several responsibilities, it validates the order, calculates the total, applies discounts, and saves the order to a database. This makes the function long and harder to understand, and any changes to one's responsibilities may affect the others, making it harder to maintain.

// Example 2: With SRP
class OrderProcessor {
  constructor(order) {
    this.order = order;
  }

  validate() {
    if (this.order.items.length === 0) {
      console.log("Error: Order has no items");
      return false;
    }
    return true;
  }

  calculateTotal() {
    let total = 0;
    this.order.items.forEach(item => {
      total += item.price * item.quantity;
    });
    return total;
  }

  applyDiscounts(total) {
    if (this.order.customer === "vip") {
      total *= 0.9;
    }
    return total;
  }
}

class OrderSaver {
  constructor(order, total) {
    this.order = order;
    this.total = total;
  }

  save() {
    const db = new Database();
    db.connect();
    db.saveOrder(this.order, this.total);
  }
}

const order = new Order();
const processor = new OrderProcessor(order);

if (processor.validate()) {
  const total = processor.calculateTotal();
  const totalWithDiscounts = processor.applyDiscounts(total);
  const saver = new OrderSaver(order, totalWithDiscounts);
  saver.save();
}

In this example, the processOrder function has been split into two classes that follow the SRP: OrderProcessor and OrderSave .

The OrderProcessor class handles the responsibilities of validating the order, calculating the total, and applying discounts, while the OrderSaver class handles the responsibilities of saving the order to the database.

This makes the code easier to understand, test, and maintain since each class has a clear responsibility and can be modified or replaced without affecting others.

Having a "Single Source of Truth"

Having a "single source of truth" means there is only one place where a particular piece of data or configuration is stored in the codebase, and any references to it in the code refer back to that one source. This is important because it ensures that the data is consistent and avoids duplication and inconsistency.

Here is an example to illustrate the concept. Let's say we have an application, that needs to display the current weather conditions in a city. We could implement this feature in two different ways.

// Option 1: No "single source of truth"

// file 1: weatherAPI.js
const apiKey = '12345abcde';

function getCurrentWeather(city) {
  return fetch(`https://api.weather.com/conditions/v1/${city}?apiKey=${apiKey}`)
    .then(response => response.json());
}

// file 2: weatherComponent.js
const apiKey = '12345abcde';

function displayCurrentWeather(city) {
  getCurrentWeather(city)
    .then(weatherData => {
      // display weatherData on the UI
    });
}

In this option, the API key is duplicated in two different files, making it harder to maintain and update. If we ever need to change the API key, we have to remember to update it in both places.

// Option 2: "Single source of truth"

// file 1: weatherAPI.js
const apiKey = '12345abcde';

function getCurrentWeather(city) {
  return fetch(`https://api.weather.com/conditions/v1/${city}?apiKey=${apiKey}`)
    .then(response => response.json());
}

export { getCurrentWeather, apiKey };


// file 2: weatherComponent.js
import { getCurrentWeather } from './weatherAPI';

function displayCurrentWeather(city) {
  getCurrentWeather(city)
    .then(weatherData => {
      // display weatherData on the UI
    });
}

In this option, the API key is stored in one place(in the weatherAPI.js file) and exported for other modules to use. This ensures there is only one source of truth for the API key and avoids duplication and inconsistency.

If we ever need to update the API key, we can do it in one place and all other modules that use it will automatically get the updated value.

Only Expose and consume the data you need

One important principle of writing clean code is to only expose and consume the information that is necessary for a particular task. This helps to reduce complexity, increase efficiency, and avoid errors that can arise from using unnecessary data.

When data is not needed to expose or consume, it can lead to performance issues and make the code more difficult to understand and maintain.

Suppose you have an object with multiple properties, but you only need to use a few of them. One way to do this would be to reference the object and the specific properties every time you need them. And this can become verbose and error-prone, especially if the object is deeply nested. A cleaner and more efficient solution would be to use object-destructing to only expose and consume the data you need.

// Original object
const user = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  age: 25,
  address: {
    street: '123 Main St',
    city: 'Anytown',
    state: 'CA',
    zip: '12345'
  }
};

// Only expose and consume the name and email properties
const { name, email } = user;

console.log(name); // 'Alice'
console.log(email); // 'alice@example.com'

Modularization

Modularization is an essential concept in writing clean code. It refers to the practice of breaking down large, complex into smaller, more manageable modules or functions. This makes the code easier to understand, test, and maintain.

Using modularization provides several benefits such as:

  1. Re-usability: Modules can be reused in different parts of the application or in other applications, saving time and effort in development.

  2. Encapsulation: Modules allow you to hide the internal details of a function or object, exposing only the essential interface to the outside world. This helps to reduce coupling between different parts of the code and improve overall code quality.

  3. Scalability: By breaking down large code into smaller, modular pieces, you can easily add or remove functionality without affecting the entire codebase.

Here is an example in Javascript of a piece of code that performs a simple task, one is not using modularization and the other is implementing modularization.

// Without modularization
function calculatePrice(quantity, price, tax) {
  let subtotal = quantity * price;
  let total = subtotal + (subtotal * tax);
  return total;
}

// Without modularization
let quantity = parseInt(prompt("Enter quantity: "));
let price = parseFloat(prompt("Enter price: "));
let tax = parseFloat(prompt("Enter tax rate: "));

let total = calculatePrice(quantity, price, tax);
console.log("Total: $" + total.toFixed(2));

In the above example, the calculatePrice is used to calculate the total price of an item given its quality, price, and tax rate. However, this function is not modularized and is tightly coupled with the user input and output logic. This can make it difficult to test and maintain.

Now, let's see an example of the same code using modularization:

// With modularization
function calculateSubtotal(quantity, price) {
  return quantity * price;
}

function calculateTotal(subtotal, tax) {
  return subtotal + (subtotal * tax);
}

// With modularization
let quantity = parseInt(prompt("Enter quantity: "));
let price = parseFloat(prompt("Enter price: "));
let tax = parseFloat(prompt("Enter tax rate: "));

let subtotal = calculateSubtotal(quantity, price);
let total = calculateTotal(subtotal, tax);
console.log("Total: $" + total.toFixed(2));

In the above example, the calculatePrice has been broken down into 2 smaller functions: calculateSubTotal and calculateTotal. These functions are now modularized and responsible for calculating the subtotal and total, respectively. This makes the code easier to understand, test, and maintain and also makes it more reusable in other parts of the application.

Modularization can also refer to the practice of dividing single files of code into many smaller files that are later compiled back onto a single (or fewer files). This practice has the same benefits we just talked about.

if you would like to know how to implement this in Javascript using modules, check it this article.

Always check null & undefined for Objects & Arrays

Neglecting null and undefined in the case of objects & arrays can lead to errors.

So, always check for them in your code:

const person = {
  name: "Haris",
  city: "Lahore",
};
console.log("Age", person.age); // error
console.log("Age", person.age ? person.age : 20); // correct
console.log("Age", person.age ?? 20); //correct

const oddNumbers = undefined;
console.log(oddNumbers.length); // error
console.log(oddNumbers.length ? oddNumbers.length : "Array is undefined"); // correct
console.log(oddNumbers.length ?? "Array is undefined"); // correct

Avoid DOM Manipulation

In React, it's generally advised to avoid DOM manipulation because React uses virtual DOM to manage updates efficiently. Directly manipulating the DOM can lead to unexpected behavior and can interfere with React's rendering optimizations.

Bad approach: Manipulating DOM directly

import React from 'react';

function InputComponent() {
  const handleButtonClick = () => {
    const inputElement = document.querySelector('input[type="text"]');
    if (inputElement) {
      inputElement.style.border = '2px solid green';
      inputElement.focus();
    }
  };

  return (
    <div>
      <input type="text" />
      <button onClick={handleButtonClick}>Focus and Highlight Input</button>
    </div>
  );
}

export default InputComponent;

Good approach: Using useRef

import React, { useRef } from 'react';

function InputComponent() {
  const inputRef = useRef(null);

  const handleButtonClick = () => {
    inputRef.current.style.border = '2px solid green';
    inputRef.current.focus();
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleButtonClick}>Focus and Highlight Input</button>
    </div>
  );
}

export default InputComponent;

Avoid Inline Styling

Inline styling makes your JSX code messy. It is good to use classes & ids for styling in a separate .css file.

const text = <div style={{ fontWeight: "bold" }}>Happy Learing!</div>; // bad approach

const text = <div className="learning-text">Happy Learing!</div>; // good approach

In .css file:

.learning-text {
  font-weight: bold;
}

7 Tips To Write Clean Function

Check it out here.

Always Remove Every Event Listener in useEffect

It's important to remove event listeners from the useEffect cleanup function to prevent memory leaks and avoid unexpected behavior in your React components:

  • Memory leaks: if you don't remove event listeners when the component unmounts, those event listeners remain in memory, even after the component is removed from the DOM. This can lead to memory leaks over time, as unused even listeners accumulate and costume memory unnecessarily.

  • Unexpected behavior: Even listeners attached to elements can cause unexpected behavior if they continue to exist after the components are unmounted

import React, { useEffect, useState } from 'react';

function ExampleComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  useEffect(() => {
    const handleClickOutside = () => {
      setCount(count + 1); // Increment count when clicked outside
    };

    document.addEventListener('click', handleClickOutside);

    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [count]); // Re-run effect when count changes

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
}

export default ExampleComponent;

Don't throw your files Randomly

Keep the related files in the same folder instead of making files in a single folder. When files are organized logically in the same folder, it's easy to maintain and update them. Developers know where to find the related code and make changes, reducing the risk of introducing bugs or inconsistencies.

For example, if you want to create a navbar in React then you should create a folder and place .js & .css & .test.js files related to the navbar in it.

Create a habit of Writing helper functions

Creating a habit of writing helper functions in ReactJS offers several advantages:

  1. Code Reusability: Helper functions encapsulate common logic or operations, allowing you to reuse them across different components or modules. This reduces code duplication and promotes a more modular and maintainable codebase.

  2. Improved Readability: By breaking down complex logic into smaller, more manageable helper functions, your code becomes easier to read and understand. Well-named helper functions serve as self-documenting code, conveying their purpose and functionality at a glance.

  3. Simplifying Component Logic: Writing helper functions enables you to offload non-UI-related logic from your components. This keeps your components focused on rendering UI elements and handling user interactions, leading to cleaner and more concise component code.

  4. Facilitating Testing: Helper functions can be tested independently, which makes it easier to write unit tests for your application logic. This promotes code reliability and helps catch bugs early in the development process.

  5. Encouraging Code Organization: By abstracting common operations into helper functions, you can better organize your codebase and adhere to principles of separation of concerns. This makes it easier to maintain and scale your application over time.

Overall, incorporating helper functions into your ReactJS projects promotes code reuse, readability, maintainability, testability, and code organization, ultimately leading to more efficient and robust applications.

Use ternary operator instead of if/else if statements

Using if else if statements make your code bulky. Instead, try to use a ternary operator where possible to make code simpler and cleaner.

// Bad approach
if (name === "Ali") {
  return 1;
} else if (name === "Bilal") {
  return 2;
} else {
  return 3;
}

// Good approach
name === "Ali" ? 1 : name === "Bilal" ? 2 : 3;

Make index.js File Name to minimize importing complexity

if you have a file named index.js in a directory named actions and you want to import action from it in your component, your import would be like this:

import { actionName } from "src/redux/actions";

actions the directory path is explained in the above import. Here you don't need to mention index.js after actions like this:

import { actionName } from "src/redux/actions/index";

Using Import Aliases

Import aliases simplify import statements, making them more readable and manageable, especially in large projects. Here’s how to use them effectively in Node.js, React, and Next.js 14.

Good Practice: Using Import Aliases

// In a React/Next.js project
import Button from '@components/Button';

// In a Node.js project
const dbConfig = require('@config/db');

Bad Practice: Without Import Aliases

// Complex and lengthy relative paths
import Button from '../../../components/Button';
const dbConfig = require('../../config/db');

Setting up Aliases

  • React/Next.js: Configure jsconfig.json or tsconfig.json for alias paths.

  • Node.js: Use module-alias package or configure package.json for custom paths.

Import aliases streamline project structure by reducing the complexity of import statements, and enhancing code readability and maintainability.

Effective Color Management

Proper color management is essential in web development to maintain a consistent and scalable design. This document outlines best practices for managing colors using Tailwind CSS, CSS variables, and JSX. It also highlights common pitfalls to avoid.

Bad practices to avoid: Inline color definitions

/* Bad Practice in css */
.some-class {
  color: #333333; /* Direct color definition */
  background-color: #ffffff; /* Hardcoded color */
}

/* Bad Practice in JSX */
const MyComponent = () => (
  <div style={{ color: '#333333', backgroundColor: '#ffffff' }}>
    Content
  </div>
);

Using CSS variables for Global Color Management

CSS variables offer a flexible and maintainable approach to managing colors globally.

// Defining CSS variables
:root {
  --primary-color: #5A67D8;
  --secondary-color: #ED64A6;
  --text-color: #333333;
  --background-color: white;
  --warning-color: #ffcc00;
}

// Using CSS variables in stylesheet
.header {
  background-color: var(--primary-color);
  color: var(--background-color);
}

// Dark Mode example with CSS variables 
.dark {
  --primary-color: #9f7aea;
  --background-color: #2d3748;
  --text-color: #cbd5e0;
}

Tailwind CSS for consistent color usage

Tailwind CSS provides a utility-first approach, allowing you to define a color palette in your configuration and set it through your project.

//tailwind.config.js 
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#5A67D8',
        secondary: '#ED64A6',
        // other colors...
      },
    },
  },
  // other configurations...
};

// Using Tailwind classes in JSX
const MyComponent = () => (
  <h1 className="text-primary bg-secondary">Welcome to My Site</h1>
);

Efficient Code Structure In React Components

Organizing code within React components in a logical or efficient manner is crucial for readability and maintainability. This guide outlines the recommended order and structure for various elements within a React component.

Recommended structure inside React components

  1. Variables and Constants: Declare any constants or variables at the beginning of the component.

     const LIMIT = 10;
    
  2. State Management & other hooks (Redux, Context): Initialize Redux hooks or Context API hooks next.

     const user = useSelector(state => state.user);
    
  3. Local State (useState, useReducer): Define local state hooks after state management hooks.

     const [count, setCount] = useState(0);
    
  4. Effects (useEffect): Place useEffect hooks after state declarations to capture component lifecycle events.

     useEffect(() => {
       loadData();
     }, [dependency]);
    
  5. Event Handlers and Functions: Define event handlers and other functions after hooks.

     const handleIncrement = () => {
       setCount(prevCount => prevCount + 1);
     };
    

Example of good structure ✅

import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import SomeService from './SomeService';
import './Page.css';

const Page = ({ variant, ...props }) => {
    // Constants
    const MAX_COUNT = 10;

    // Redux State
    const user = useSelector(state => state.user);

    // Local State
    const [count, setCount] = useState(0);
    const [data, setData] = useState(null);

    // useEffect for loading data
    useEffect(() => {
        SomeService.getData().then(data => setData(data));
    }, []);

    // useEffect for user-related operations
    useEffect(() => {
        if (user) {
            console.log('User updated:', user);
        }
    }, [user]);

    // Event Handlers
    const handleIncrement = () => {
        if (count < MAX_COUNT) {
            setCount(prevCount => prevCount + 1);
        }
    };

    return (
        <div className={`page page-${variant}`}>
            <h1>Welcome, {user.name}</h1>
            <button onClick={handleIncrement}>Increment</button>
            <p>Count: {count}</p>
            {data && <div>Data loaded!</div>}
        </div>
    );
};

export default Page;

Best Practices in Code Documentation

Effective documentation is key to making code readable and maintainable. This guide covers the usage of JSDoc and the dos and don'ts of commenting.

JSDoc for Javascript

JSDoc is a popular tool for documenting Javascript code. It helps in understanding the purpose of functions, parameters, and return types.

Good JSDoc Example

/**
 * Adds two numbers together.
 * @param {number} a - The first number.
 * @param {number} b - The second number.
 * @returns {number} The sum of the two numbers.
 */
function sum(a, b) {
  return a + b;
}

Bad JSDoc Example

// Adds a and b
function sum(a, b) {
  return a + b;
}
// Missing detailed JSDoc comment

Including meaningful comments & avoid redundancy

Strategic comments enhance code clarity but beware of redundancy. Prioritize meaningful insights to facilitate collaboration and understanding among developers.

Good practice

// Loop through users and apply discounts to eligible ones
users.forEach(user => {
  if (user.isEligibleForDiscount()) {
    applyDiscount(user);
  }
});

// --------------------------------------------

// Calculate the area of a rectangle
function calculateArea(length, width) {
  return length * width;
}

Bad Practice

// Start a loop
users.forEach(user => {
  // Check if the user is eligible for discount
  if (user.isEligibleForDiscount()) {
    // Apply discount to the user
    applyDiscount(user);
  }
});
// Redundant comments that simply restate the code

// ----------------

// Calculate area
function calculateArea(l, w) {
  return l * w;
  // Ambiguous and unhelpful comment
}

Secure coding Practices

Security is a paramount aspect of web development. Writing secure code is crucial to protect against vulnerabilities like SQL injection, XSS (Cross-Site Scripting), and CSRF (Cross-Site Request Forgery).

  • Protecting Against XSS Attacks

    Cross-site scripting (XSS) attacks occur when malicious scripts are injected into web pages viewed by other users. This can lead to data theft, session hijacking, and other security breaches. To learn more about XSS

    Vulnerable Code Example:

      // Rendering user input directly to the DOM
      document.getElementById("user-content").innerHTML = userInput;
    

    Secure Code Example

      // Escaping user input before rendering
      const safeInput = escapeHtml(userInput);
      document.getElementById("user-content").textContent = safeInput;
    
      // Example: Using DOMPurify to sanitize user input
      const cleanInput = DOMPurify.sanitize(userInput);
      document.getElementById("user-content").innerHTML = cleanInput;
    
  • Mitigating CSRF Attacks

    CSRF attacks force a logged-on victim to submit a request to a web application on which they are currently authenticated. These attacks can be used to perform actions on behalf of the user without their consent. To learn more about CSRF.

    Vulnerable Code Example

      <!-- GET request for sensitive action -->
      <a href="/delete-account">Delete Account</a>
    

    Secure Code Example

      // Backend: Generate and validate CSRF tokens
      app.use(csrfProtection);
      app.post("/delete-account", (req, res) => {
        // Validate CSRF token
      });
    
      <!-- Frontend: Include CSRF token in form -->
      <form action="/delete-account" method="POST">
        <input type="hidden" name="_csrf" value="{csrfToken}" />
        <button type="submit">Delete Account</button>
      </form>
    
  • Usingnpm-auditto Identify Vulnerabilities

    Run npm audit to identify insecure dependencies. Regularly update your package to the latest, non-vulnerable versions.

  • Incorporating Synk for Continuous Security

    Integrate Snyk into your development workflow for continuous monitoring and fixing of vulnerabilities in dependencies.

  • Managing Environment Variables Securely

    Store sensitive information like API keys and passwords in .env files and access them via process.env in your code.

      // Bad Practice: Hardcoded secret
      const API_KEY = "hardcoded-secret-key";
    
      // Good Practice: Example of accessing a secret from .env file
      require("dotenv").config();
      const API_KEY = process.env.API_KEY;
    

Follow the SOLID Principle

“Single Responsibility”, “Open/Closed”, “Liskov Substitution”, “Interface Segregation”, and “Dependency Inversion” - these five principles (SOLID for short) are the cornerstones of writing code that scales and is easy to maintain.

Utilize Design Patterns, but don't over-design

Design patterns can help us show some common problems. However, every pattern has its applicable scenarios. Overusing or misusing design patterns may make your code more complex and difficult to understand.

Below are these design patterns you should know:

  • Factory

  • Behavioral

  • Strategy

  • Proxy

  • Structural

  • Adapter

  • Singleton

  • Creational

Folder Structure

Having an organized folder structure is super important if you want to keep your project hierarchy clear and make it easy to navigate.

You can check out this article to explore how to organize react folder structure.

Apply Custom Hook Pattern

The Custom Hook pattern in React is a technique that allows encapsulating the logic of a component in a reusable function. Custom Hooks are javascript functions that use Hooks provided by React(such as useState, useEffect, useContext, etc) and can be shared between components to effectively encapsulate and reuse logic.

When to use it

  • When you need to share the logic between React components without resorting to code duplication.

  • To abstract the complex logic of a component and keep it more readable and easier to maintain.

  • When you need to modularize the logic of a component to facilitate unit testing.

When not to use it

  • When the logic is specific to a single component and will not be reused elsewhere.

  • When the logic is simple and does not justify the creation of a Custom Hook.

Advantages

  • Promotes code reuse by encapsulating common logic in separate functions.

  • Facilitates code composition and readability by separating the logic from the component.

  • Improves testability by enabling more specific and focused unit tests on the logic encapsulated in Custom Hooks.

Disadvantages

  • This may result in additional complexity if abused and many Custom Hooks are created

  • Requires a solid understanding of React and Hooks concepts for proper implementation.

Example

Here is an example of a Custom Hook that performs a generic HTTP request using Typescript and React. This hook handles the logic to make the request and handles the load status, data, and errors.

import { useState, useEffect } from 'react';
import axios, { AxiosResponse, AxiosError } from 'axios';

type ApiResponse<T> = {
  data: T | null;
  loading: boolean;
  error: AxiosError | null;
};

function useFetch<T>(url: string): ApiResponse<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<AxiosError | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response: AxiosResponse<T> = await axios.get(url);
        setData(response.data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Cleanup function
    return () => {
      // Cleanup logic, if necessary
    };
  }, [url]);

  return { data, loading, error };
}

// Using the Custom Hook on a component
function ExampleComponent() {
  const { data, loading, error } = useFetch<{ /* Expected data type */ }>('https://example.com/api/data');

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (!data) {
    return <div>No data.</div>;
  }

  return (
    <div>
      {/* Rendering of the obtained data */}
    </div>
  );
}

export default ExampleComponent;

In this example, the Custom Hook useFetch takes a URL as an argument and performs a GET request using Axios. It manages the load status, data, and errors, returning an object with this information.

The ExampleComponent component uses the Custom Hook useFetch to fetch data from an API and render it in the user interface. Depending on the status of the request, a load indicator, an error message, or the fetched data is displayed.

There are many ways to use this pattern, in this link you can find several examples of custom Hooks to solve specific problems, the uses are many.

Apply HOC Pattern

The High Order Component(HOC) pattern is a composition technique in React that is used to reuse the logic between components. A HOC is a function that takes a component and returns a new component with additional and extended functionality.

When to use it

  • When you need to share logic between multiple components without duplicating code.

  • To add common behaviors or features to multiple components.

  • When you want to isolate presentation logic from business logic in a component.

When not to use it

  • When the logic is specific to a single component and will not be reused.

  • When the logic is too complex and may make HOCs difficult to understand.

Advantages

  • Promotes code reuse by encapsulating and sharing logic between components.

  • Allows clear separation of presentation logic from business logic.

  • Facilitates code composition and modularity by applying function design patterns.

Disadvantages

  • May introduce an additional layer of abstraction that makes it difficult to track data flow.

  • Excessive compositions of HOCs can generate complex components that are difficult to debug.

  • Sometimes, It can hide the component hierarchy, making it difficult to understand how the application is structured.

Example

Suppose we want to create an HOC that handles the state and methods for submitting data from a form. The HOC will handle the form values, validate the data, and send a request to the server.

import React, { ComponentType, useState } from 'react';

interface FormValues {
  [key: string]: string;
}

interface WithFormProps {
  onSubmit: (values: FormValues) => void;
}

// HOC that handles form state and logic
function withForm<T extends WithFormProps>(WrappedComponent: ComponentType<T>) {
  const WithForm: React.FC<T> = (props) => {
    const [formValues, setFormValues] = useState<FormValues>({});

    const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      const { name, value } = event.target;
      setFormValues((prevValues) => ({
        ...prevValues,
        [name]: value,
      }));
    };

    const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      props.onSubmit(formValues);
    };

    return (
      <WrappedComponent
        {...props}
        formValues={formValues}
        onInputChange={handleInputChange}
        onSubmit={handleSubmit}
      />
    );
  };

  return WithForm;
}

// Component that uses the HOC to manage a form.
interface MyFormProps extends WithFormProps {
  formValues: FormValues;
  onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const MyForm: React.FC<MyFormProps> = ({ formValues, onInputChange, onSubmit }) => {
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" value={formValues.name || ''} onChange={onInputChange} />
      <input type="text" name="email" value={formValues.email || ''} onChange={onInputChange} />
      <button type="submit">Enviar</button>
    </form>
  );
};

// Using the HOC to wrap the MyForm component
const FormWithLogic = withForm(MyForm);

// Main component that renders the form
const App: React.FC = () => {
  const handleSubmit = (values: FormValues) => {
    console.log('Form values:', values);
    // Logic to send the form data to the server
  };

  return (
    <div>
      <h1>HOC Form</h1>
      <FormWithLogic onSubmit={handleSubmit} />
    </div>
  );
};

export default App;

In this example, the withForm HOC encapsulates the logic for handling a form. This HOC handles the state of the form values, and provides a function to update the form values (handleInputChange), and a function to handle the form submission (handleSubmit). Then, the HOC is used to wrap the MyForm component, which is the form that will be rendered in the main application (App).

Apply Extensible Styles Pattern

The Extensible Styles pattern is a technique that allows the creation of React components with flexible and easily customizable styles. Instead of applying styles directly to the component, this pattern uses dynamic CSS properties or classes that can be modified and extended according to the user’s needs.

When to use it

  • When you need to create components that can adapt to different styles or themes within an application.

  • To allow end users to easily customize the appearance of components.

  • When you want to maintain visual consistency in the user interface while providing flexibility in the appearance of components.

When not to use it

  • When style customization is not a concern or styles are not expected to vary significantly.

  • In applications where tight control over the styles and appearance of components is required.

Advantages

  • Facilitates customization and extension of styles in components without the need to modify the source code.

  • Maintains visual consistency in the application while providing flexibility in styles.

  • Simplifies maintenance by separating the styling logic from the component code.

Disadvantages

  • May result in increased complexity if extensible styles are not managed properly.

  • Requires careful design to ensure that styles can be extended in a consistent and predictable manner.

Example

Suppose we want to create a button component with extensible styles that allows changing its color and size by means of props.

import React from 'react';
import './Button.css';

interface ButtonProps {
  color?: string;
  size?: 'small' | 'medium' | 'large';
  onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({ color = 'blue', size = 'medium', onClick, children }) => {
  const buttonClasses = `Button ${color} ${size}`;

  return (
    <button className={buttonClasses} onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;
.Button {
  border: none;
  cursor: pointer;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 14px;
  font-weight: bold;
}

.small {
  padding: 4px 8px;
}

.medium {
  padding: 8px 16px;
}

.large {
  padding: 12px 24px;
}

.blue {
  background-color: blue;
  color: white;
}

.red {
  background-color: red;
  color: white;
}

.green {
  background-color: green;
  color: white;
}

In this example, the Button component accepts properties such as color and size, which can be used to customize its appearance. The CSS styles are defined in an extensible way, allowing the size and color of the button to be easily modified by prop. This provides flexibility for the developer to adapt the component to different styles within the application.

Apply Compound Components Pattern

The Compound Components Pattern is a design technique in React that allows the creation of components that work closely and coherently together. In this pattern, a parent component can encapsulate multiple child components, enabling seamless communication and coordinated interaction among them.

When to use

  • When you need to create components that depend on each other and perform better when grouped together.

  • To build highly customizable and flexible components that can adapt to different use cases.

  • When you want to maintain a clear and organized component structure in the React component tree hierarchy.

When not to use

  • In cases where the relationship between components is not close or there is no clear dependency between them.

  • In situations where the added complexity of the Compound Components pattern does not justify its benefits.

Advantages

  • Facilitates encapsulation and reuse of related logic in a set of components.

  • Provides a clear and consistent API for interacting with compound components.

  • Allows for greater flexibility and customization by combining multiple components into one.

Disadvantages

  • Can introduce additional complexity in understanding how components interact with each other.

  • Requires careful design to ensure that compound components are flexible and easy to use.

Example

Suppose we want to create a Tabs component that encapsulates tabs (Tab) that can be shown and hidden according to the selected index.

import React, { useState, ReactNode } from 'react';

interface TabProps {
  label: string;
  children: ReactNode;
}

const Tab: React.FC<TabProps> = ({ children }) => {
  return <>{children}</>;
};

interface TabsProps {
  children: ReactNode;
}

const Tabs: React.FC<TabsProps> = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div className="tab-header">
        {React.Children.map(children, (child, index) => {
          if (React.isValidElement(child)) {
            return (
              <div
                className={`tab-item ${index === activeTab ? 'active' : ''}`}
                onClick={() => setActiveTab(index)}
              >
                {child.props.label}
              </div>
            );
          }
        })}
      </div>
      <div className="tab-content">
        {React.Children.map(children, (child, index) => {
          if (index === activeTab) {
            return <>{child}</>;
          }
        })}
      </div>
    </div>
  );
};

const Example: React.FC = () => {
  return (
    <Tabs>
      <Tab label="Tab 1">
        <div>Contenido de la pestaña 1</div>
      </Tab>
      <Tab label="Tab 2">
        <div>Contenido de la pestaña 2</div>
      </Tab>
      <Tab label="Tab 3">
        <div>Contenido de la pestaña 3</div>
      </Tab>
    </Tabs>
  );
};

export default Example;

Use the Atomic Design Pattern to structure your React application

As we build scalable applications in React, we often encounter challenges in managing the growing complexity of component structures. The Atomic Design Pattern has emerged as a powerful methodology for organizing and structuring applications. This pattern, inspired by chemistry, breaks downs the interface into multiple fundamental building blocks, promoting a more modular and scalable approach to application design. It serves as a powerful methodology to enhance the readability, maintainability, and flexibility of our application code.

The Atomic Design Pattern was introduced by Brad Frost and Dave Olsen and is based on the idea that a design system should be broken down into its smallest parts, which are used to build up increasingly complex and reuseable components. The goal is not to create a strict hierarchy but rather to provide a mental model to better understand and create user interfaces.

The Atomic Design methodology breaks down design into 5 distinct levels:

  • Atoms: These are the basic building blocks of your application, like an input field, a button, or a form label. In react, these would represented as individual components. They serve as foundational elements that are not exactly useful on their own but are fundamental for building more complex components.

  • Molecules: Molecules are groups of atoms that are combined together to form a functional unit. For example, a form might be a molecule that includes atoms like labels, an input field, and a submit button.

  • Organisms: Organisms are relatively complex UI components composed of groups of molecules and/or atoms. These are langer sections of an interface like a header, footer, or navigation bar and therefore can have their own states and functionality.

  • Templates: Templates are page-level objects that place components into a layout and articulate the design’s underlying content structure. They usually consist of groups of organisms, representing a complete layout.

  • Pages: Pages are specific instances of templates that show what a UI looks like with real representative content in place. These pages serve as ecosystems that display different template renders.

To implement Atomic Design in a React application, we can consider the following key points:

  • Component Categorization: Organize the components into atoms, molecules, organisms, templates, and pages. This categorization should be reflected in our project’s file structure.

  • State Management: we also need to decide how the state should be managed across different levels of components. Atoms and molecules might not hold state, while organisms and templates might need to.

  • Documentation: It’s important to have thorough documentation of its components and usage. This can be facilitated by tools like Storybook, as this will allow us to create a living style guide.

Let’s see a very simple example of how a React application built on the atomic design principle would structurally look like.

import React from 'react';

const noop = () => {};

// Atoms
const Button = ({ onClick, children, type }) => <button type={type} onClick={onClick}>{children}</button>;
const Label = ({ htmlFor, children }) => <label htmlFor={htmlFor}>{children}</label>;
const Input = ({ id, type, onChange, value = "" }) => <input id={id} type={type} onChange={onChange} value={value} />;
const Search = ({ onChange }) => <input type="search" onChange={onChange} />;
const NavMenu = ({ items }) => <ul>{items.map((item) => <li>{item}</li>)}</ul>;

// Molecules
const Form = ({ onSubmit }) => (
  <form onSubmit={onSubmit}>
    <Label htmlFor="email">Email:</Label>
    <Input id="email" type="email" onChange={noop} />
    <Button type="submit" onClick={noop}>Submit</Button>
  </form>
);

// Organisms
const Header = () => (
  <header>
    <Search onChange={noop} />
    <NavMenu items={[]} />
  </header>
);

const Content = ({ children }) => (
  <main>
    {children}
    <Form onSubmit={noop} />
  </main>
);

// Templates
const MainTemplate = ({ children }) => (
  <>
    <Header />
    <Content>{children}</Content>
  </>
);

// Pages
const HomePage = () => (
  <MainTemplate>
    <h2>My Form</h2>
    <p>This is a basic example demonstrating Atomic Design in React.</p>
  </MainTemplate>
);

Why Atomic Design?

The Atomic Design Pattern aligns perfectly with React’s component-based architecture. It allows us to:

  • Promote reusability: By breaking down the interface into the smallest parts, it becomes easier to reuse components and leverage modular composition across different parts of an application or even across different projects.

  • Ensure Consistency: Atomic Design helps maintain UI consistency, which is crucial for user experience and brand identity.

  • Facilitates Maintenance: When components are well-organized, it becomes much simpler to update or maintain them over time.

  • Improve Collaboration: A shared design language based on Atomic Design Principles can enhance communications, usage, and contributions since it is easier to understand the codebase.

  • Promotes Code Quality: As we create a sub-ecosystem for each component feature, each component or service has its isolated environment, including styles, actions, and tests. This isolation makes testing more effective and ensures consistent code quality.

While Atomic Design offers many benefits, we would want to ensure that we implement this principle to our advantage and not over-engineer. It can be easy to over-abstract components, which can lead to unnecessary complexity. Therefore, we should also keep an eye on performance implications when breaking down components into smaller pieces to reap the full benefits of this technique.

Use Barrel Exports To Export React Components

When you are working on a large React project, you might have different folders containing different components.

In such cases, if you are using different components in a particular file, your file will contain a lot of import statements like this:

import ConfirmModal from './components/ConfirmModal/ConfirmModal'; 
import DatePicker from './components/DatePicker/DatePicker'; 
import Tooltip from './components/Tooltip/Tooltip';
import Button from './components/Button/Button';
import Avatar from './components/Avatar/Avatar';

which does not look good as the number of components increases, the number of import statements will also increase.

To fix this issue, you can create an index.js file in the parent folder (components) and export all the components as the named export from that file like this.

export { default as ConfirmModal } from './ConfirmModal/ConfirmModal'; 
export { default as DatePicker } from './DatePicker/DatePicker'; 
export { default as Tooltip } from './Tooltip/Tooltip';
export { default as Button } from './Button/Button';
export { default as Avatar } from './Avatar/Avatar';

This needs to be done only once. Now, if in any of the files you want to access any component, you can easily import it using the named import in a single file like this:

import {ConfirmModal, DatePicker, Tooltip, Button, Avatar} from './components';

which is the same as

import {ConfirmModal, DatePicker, Tooltip, Button, Avatar} from './components/index';

This is standard practice when working on large industry/company projects.

This pattern is known as the barrel pattern which is a file organization pattern that allows use to export all modules in a directory in a single file.

Here is a CodeSandbox demo to see it in action.

Use console.count Method To Find Out Number Of Re-Renders Of Components

Sometimes we want to know, how many times the line of a particular code is executed.

Maybe we want to know how many times a particular function is getting executed.

In that case, we can use a console.count method by passing a unique string to it as an argument.

For example, If you have a React code and you want to know how many times the component is getting re-rendered then instead of adding console.log and manually counting how many times it’s printed in the console, you can just add console.count('render') in the component.

Ad you will see the render message along with the count of how many times it’s executed.

Avoid Passing setState function as A Prop To the Child Component

Never pass the setState function directly as a prop to any of the child components like this:

const Parent = () => {
 const [state, setState] = useState({
   name: '',
   age: ''
 })

 .
 .
 .

 return (
  <Child setState={setState} />
 )
}

The state of a component should be only changed by that component itself.

Here’s why:

  • This ensures the code is predictable. If you pass the setState directly to multiple components, it will be difficult to identify where the state is getting changed.

  • This lack of predictability can lead to unexpected behavior and makes debugging code is difficult.

  • Over time, as your application grows, you may need to refactor or change how the state is managed in the parent component.

  • if child components rely on direct access to setState, these changes can ripple through the codebase and require updates in multiple places, increasing the risk of introducing bugs.

  • If the sensitive data is part of the state, directly passing useState could potentially expose that data to child components, increasing security risks.

  • React’s component reconciliation algorithm works more efficiently when state and props updates are clearly defined within components.

Instead of passing setState directly, you can do the following:

Pass data as props: Pass the data that the component needs as props, not the setState function itself. This way, you provide a clear interface for the child component to receive data without exposing the implementation details of the state.

Pass function as props: If the child component needs to interact with the parent component’s state, you can pass the function as props. Declare a function in the parent component and update the state in that function, you can pass this function as a prop to child components and call it on the child component when needed.

Dynamically Adding Tailwind Classes In React Does Not Work

if you are using Tailwind CSS for styling and you want to dynamically add any class then the following code will not work.

<div className={`bg-${isActive ? 'red-200' : 'orange-200'}`}>
  Some content
</div>

This is because in your final CSS file, Tailwind CSS includes only the classes present during its initial scan of your file.

So the code above will dynamically add the bg-red-200 or bg-orange-200 class to the div but its CSS will not be added so you will not see the classes applied in your div.

So to fix this, you need to define the entire class initially like this:

<div className={`${isActive ? 'bg-red-200' : 'bg-orange-200'}`}>
  Some content
</div>

if you have a lot of classes that need to be conditionally added, then you can define an object with the complete class names like this:

const colors = {
  purple: 'bg-purple-300',
  red: 'bg-red-300',
  orange: 'bg-orange-300',
  violet: 'bg-violet-300'
};

<div className={colors['red']}>
  Some content
</div>

Wrapping up

I hope you enjoyed the article and learned something new :)))

Happy coding!!!!!!

References

https://dev.to/sathishskdev/part-4-writing-clean-and-efficient-react-code-best-practices-and-optimization-techniques-423d

https://dev.to/perssondennis/react-anti-patterns-and-best-practices-dos-and-donts-3c2g

https://najm-eddine-zaga.medium.com/18-best-practices-for-react-617e23ed7f2c

https://www.freecodecamp.org/news/best-practices-for-react/

https://www.freecodecamp.org/news/how-to-write-clean-code/#clear-flow-of-execution

https://dev.to/iambilalriaz/react-best-practices-ege?ref=dailydev

https://peacockindia.mintlify.app/introduction

https://baguilar6174.medium.com/react-design-patterns-6ab55c5ebafb

https://www.freecodecamp.org/news/react-best-practices-ever-developer-should-know/?ref=dailydev

https://rjroopal.medium.com/atomic-design-pattern-structuring-your-react-application-970dd57520f8

https://dev.to/_ndeyefatoudiop/101-react-tips-tricks-for-beginners-to-experts-4m11?context=digest

https://dev.to/myogeshchavan97/top-25-react-tips-every-developer-should-know-part-2-2ba7?context=digest

0
Subscribe to my newsletter

Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tuan Tran Van
Tuan Tran Van

I am a developer creating open-source projects and writing about web development, side projects, and productivity.