ReactJS Good Practices

Nicole Zonnenberg - Nov 1 '23 - - Dev Community

Written as an exercise for my engineering cohort.

Introduction

React is a free and open-source front-end JavaScript library for building user interfaces based on components and state management. It is maintained by Meta and can be used to develop single-page, mobile, or server-rendered applications with frameworks like NextJS.

ReactJS is a highly flexible framework that offers a wide range of features and capabilities to create highly interactive and dynamic user interfaces. It gives developers the flexibility to choose the best approach for their specific case, allowing for highly customizable and scalable applications. However, with this flexibility comes the responsibility to follow good practices to ensure the code is maintainable, efficient, and optimized.

This document is a record of “good practices”–not “best practices”–as no project utilizing ReactJS has the same requirements or constraints. These are the methods we recommend that will produce good outcomes, but are not strict standards.

At the end of this doc, you’ll also find a list of additional materials–resources, repos, tools, and playgrounds–where you can learn more about ReactJS and play with it.

Our ReactJS good practices

Componentization

TLDR: Break your application into smaller, simpler, reusable components.

A component is an independent and reusable piece of code. It serves the same purpose as JavaScript functions, but works in isolation and returns HTML.

Each component should have a specific purpose and should only be responsible for rendering that particular functionality. A component should be reusable, easy to understand, and follow best practices for state management and separation of concerns.

Class vs Functional Components

Use functional components as much as possible, as they are simpler, more concise, and easier to test. Use class components when you need to implement lifecycle methods, stateful logic, or when you need to access refs when React Hooks does not support certain functions. For example: getSnapshotBeforeUpdate, getDerivedStateFromError (such as in Error Boundaries) and componentDidCatch (source).

Class Component

Similar to functional component but has some additional features. The functional component does not care about the components in your app whereas the class components can work with each other.

✔ Good Example - A class component that extends the Component class and has a render method that returns a simple JSX element.

class Welcome extends Component {
  render () {
    return <h1>Hello, {this.props.welcome}</h1>
  }
}
Enter fullscreen mode Exit fullscreen mode

✖ Bad Example - The below example has a constructor method and binds the handleInputChange method to the component instance, which can cause unnecessary overhead. The component is also not reusable because it has hardcoded logic for setting the name state.

class Welcome extends Component {
  constructor(props) {
    super(props);
    this.state = {
      welcome: props.welcome
    };
    this.handleInputChange = this.handleInputChange.bind(this);
  }
  handleInputChange(event) {
    this.setState({ welcome: event.target.value });
  }
  render() {
    return (
      <div>
        <h1>{this.state.welcome}</h1>
        <input type="text"
               value={welcome}
               onChange={handleInputChange} />
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Functional Component

Functional Components are JavaScript functions. Efficient functional components are used only when the component is not required to interact with any other components or require data from other components. They can inherit variables when called.

We will often recommend functional components over class components, as functional components are simpler and easier to test.

✔ Good Example - Takes an object as an argument and returns a simple JSX element; is reusable and easy to understand.

function Welcome(props) {
  return <h1>{props.welcome}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Testing the above code would only require the test of the component’s single function (in this case, the welcome header).

✖ Bad Example - While the below code is equally as valid as the example above, it is more complicated and not single function. In this component, we’re not only dealing with showing the welcome header but also the input where the user can input a new name, the handling of that change, and state management.

Testing the code below would require not only the header, but also the input, the event handler, and the state change.

While that might seem manageable, code and applications can get complicated quickly. So for scalable purposes, keeping the components simple is a necessity.

function Welcome(props) {
  const [welcome, setWelcome] = useState('');
  const handleInputChange = (event) => {
    setName(event.target.value);
  }
  return (
    <div>
      <h1>{welcome}</h1>
      <input type="text"
             value={welcome}
             onChange={handleInputChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Presentational vs. Container Components

Presentational and container components can be either functional or class components. What separates these two components is their function in the overall application.

Presentational Component

Presentational components are concerned with how things look. It receives data and behavior from parent components.

Container Component

Container components are concerned with how things work. They provide the data and behavior to presentational or other container components.

Function Types

General Rules (TLDR):

Arrow Functions

While any arrow function can be replaced by a standard function, there would be little to gain from doing so.

Why use arrow functions almost everywhere?

  • Scope safety: When arrow functions are consistently used, everything is guaranteed to use the same thisObject as the root. A single standard function in combination with arrow functions can mess up the scope.

  • Code readability: Arrow functions are easier to read and write. Also, when almost everything is an arrow function, any regular (named) function stands out for defining the scope.

✔ Good Example - The below is an example of code that is simple and readable. It’s not hard to understand what is happening.

var comments = [];
articles.getList()
    .then(articles => Promise.all(
      articles.map(article => article.comments.getList())))
    .then(commentsLists => commentLists.reduce((a, b) => a.concet(b)));
    .then(comments => {
        this.comments = comments;
    })
}
Enter fullscreen mode Exit fullscreen mode

✖ Bad Example - The below code works the same as above and for all intents and purposes is perfectly reasonable code. However, there’s a lot of extra characters that make reading the code more difficult.

var comments = [];
articles.getList()
    .then(function (articles) {
       return Promise.all(articles.map(function (article) {
          return article.comments.getList();
       }));
    })
    .then(function (commentLists) {
       return commentLists.reduct(function (a, b) {
          return a.concat(b);
       });
    })
    .then(function (comments) {
       this.comments = comments;
    }.bind(this))
Enter fullscreen mode Exit fullscreen mode

Named Functions

Why always use regular functions on the global scope or module scope?

  • To differentiate the functions that should not access thisObject.

  • The window object (global scope) is best addressed explicitly.

  • Many Object.prototype definitions live in the global scope (i.e. String.prototype.truncate, etc.). These generally have to be named functions and can help avoid errors.

  • Function declarations are hoisted (meaning they can be accessed before they are declared), which is useful in a static utility function.

Note: Named functions can double as object constructors.

Object Constructors

Note: If you don’t initialize state and you don’t bind methods, you don’t need to implement a constructor for components.

Typically constructors are used for two purposes:

  • Initializing local state by assigning an object to this.state.

  • Binding event handler methods to an instance.

Do not call setState() in the constructor(). Instead, if your component needs to use local state, assign the initial state to this.state.

constructor(props) {
   super(props);
   this.state = { counter: 0 };
   this.handleClick = this.handleClick.bind(this);
}
Enter fullscreen mode Exit fullscreen mode

State Management

State should be managed in the component where it is needed, and props should be used to pass data and functionality between components.

Separation of Concerns

TLDR: Separation of concerns is the practice of separating different functionalities of an application into distinct and independent components, each responsible for a specific task.

This separation enables developers to manage and reason about different components independently, reducing the complexity of the application and making it easier to maintain and scale.

Keep State as Minimal as Possible

TLDR: State should only contain data that is necessary for the component to function.

Avoid using complex state structures to make it easier to manage and debug. There are multiple libraries to help manage complex state management such as Redux, Hookstate, etc.

React Hooks

React Hooks let you use state and other features within functional components. You must import individual hooks from react.

It is possible to create custom react hooks. Custom Hooks start with use (i.e. useFetch).

Hook Rules:

  • Hooks can only be called inside React function components.
  • Hooks can only be called at the top level of a component.
  • Hooks cannot be conditional.

Event Handling

React has its own event system. Avoid using native DOM events or directly modifying the state.

Event handlers can pass a data argument using arrow functions. This can help race conditions that can occur.

For example, useEffect has a clean up function that we can take advantage of in two ways:

  1. If there are going to be several requests, but only want to render the last result, we can use a boolean flag.
useEffect(() => {
  let active = true;
  const fetchData = async () => {
     const response = await fetch(...);
     const newData = await response.json();
     if (active) {
        setFetchedId(props.id);
        setData(newData);
     };
  };
  fetchData();
  return () => {
     active = false;
  }
}, [props.id])
Enter fullscreen mode Exit fullscreen mode

In the example above:

  • Changing props.id will cause a re-render.

  • Every re-render will trigger the clean up function to run (setting active to false).

  • When active is set to false, the older requests won’t update the state.

The above example will still have a race condition (multiple requests), but only the results from the newest request will be used.

  1. If we don’t have users on Internet Explorer, we can use AbortController.
useEffect(() => {
   const abortController = new AbortController();
   const fetchData = async () => {
      try {
         const response = await fetch(..., {
            signal: abortController.signal
         });
         const newData = await response.json();
         setFetchedId(id);
         setData(newData);
      } catch (error) {
         if (error.name === 'AbortError') {
            // Aborting a fetch throws error
            // Cannot update state
         }
         // Handle other errors
      }
   };
   fetchData();
   return () => {
      return () => {
         abortController.abort();
      }
   }
}, [id])
Enter fullscreen mode Exit fullscreen mode

In this example:

  • AbortController is initialized at the start of the effect.

  • AbortController.signal is passed to fetch as an option.

  • AbortErrors are caught in the try.

  • Abort function is called inside the clean-up function.

Unlike the former example, the abort function cancels the in-flight HTTP requests so it will only show the last successful request.

Type Checking

TLDR: It is recommended to use TypeScript when possible in addition to PropTypes for type checking. TypeScript should catch all errors in compile, but having PropTypes will catch anything that snuck through at runtime.

A robust application should check and confirm that it is receiving and returning the right types of data. One of the weaknesses of JavaScript is how easily variables can change types.

// The variable zero can be assigned the integer zero
let zero = 0;
// But then I can reassign it to a string
zero = "zero"
Enter fullscreen mode Exit fullscreen mode

Two of the most common ways to check for type checking for React are PropTypes and/or TypeScript. Both can be used separately or together; both differ in how they work.

propTypes and defaultProps

The PropTypes utility is available through the prop-types package. It is a runtime type-checking tool for props in React Applications. With PropTypes, you can define types of props expected in a component.

However, PropTypes does not provide warnings in production applications. Warnings are only printed in development.

import PropTypes from 'prop-types';
const Component = ({ fullName, age, courses, enrolled }) => {
  ...
}
Component.propTypes = {
   fullName: Proptypes.string.isRequired,
   age: Proptypes.int.isRequired,
   courses: Proptypes.array,
   enrolled: Proptypes.bool.isRequired
}
Enter fullscreen mode Exit fullscreen mode

TypeScript

TypeScript is a free and open-source programming language that adds static typing with optional type annotations to JavaScript. Unlike PropTypes, which does type-checking while the application is running in the browser, TypeScript does type-checking during compile time when the TypeScript is compiled in JavaScript. It shows errors in the integrated development environment (IDE) so the problems can be caught earlier.

Here is how the above example could be implemented in TypeScript:

import React from 'react';
interface ComponentProps {
  fullName: string;
  age: number;
  courses?: Array<string>;
  enrolled: boolean;
}
const Components: React.FC<ComponentProps> = ({
  fullName,
  age,
  courses,
  enrolled
}) => {
   // ...
}
Enter fullscreen mode Exit fullscreen mode

This means that the application will not compile if the code tries to assign a variable that has been assigned one data type (re: a number) another data type (re: a string).

Note: Both TypeScript and PropTypes will not catch these issues in production.

Debugging

React Developer Tools

TLDR: Browser extension for debugging and inspecting React components. It helps visualize components and their state in real time.

The easiest way to debug websites built with React is to install the React Developer Tools browser extension. It is available for several popular browsers:

Testing

Recommended Testing Frameworks & Utilities:

Tips & Tricks

Avoid Copying Props to State

⚠ This is a common mistake:

constructor(props) {
   super(props);
   this.state = { color: props.color };
}
Enter fullscreen mode Exit fullscreen mode

The problem is that it’s both unnecessary (you can use this.props.color directly) and creates bugs (updates to the color prop won’t be reflected in the state).

Error Boundaries

Class components that catch all errors/exceptions thrown at children component levels.

Note: ErrorBoundaries should be declared as class components because they do not support functional components.

// ErrorBoundary Component
class ErrorBondary extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hasErrors: false
    };
  }
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render () {
    if (this.state.hasError) {
      return <ErrorMessage />;
    }
    return this.props.children;
  }
}
Enter fullscreen mode Exit fullscreen mode

Linting

React Fragments

Individual components can only return a single element. With Fragment (imported from react), you can group elements together but it does not add any element in the DOM.

import { Fragment } from 'react';
const Frag = () => {
  return (
    <Fragment>
      <ComponentA />
      <ComponentB />
    </Fragment>
  )
}
// Or after Babel 7
const NewFrag = () => {
  return (
    <>
      <ComponentA />
      <ComponentB />  
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Additional materials

Official Resources

Certifications & Courses

Communities

Frameworks

Playgrounds

Resources and Tools

. . . . . . . . . . .