React Hooks Best Practices.

React Hooks Best Practices.

·

10 min read

WHAT YOU'LL LEARN

  1. What React and React Hooks are.

  2. Best Practices you can employ in your react projects.

React is a frontend JavaScript library used for building user interfaces by breaking your application UI into small parts called components.

Components are javaScript functions that return a markup - an html-like code. React is used for building single-page applications which follow the declarative programming paradigm.

React is maintained by Meta and a family of other open-source developers.

Since its inception, stateful components ( components that have memory ) were built with classes and presentational components were built with functional components, sometimes.

On February 16, 2019, Meta released React v16.8 which came with new features, The most notable one is Hooks which makes it possible to write stateful components with functions.

React Hooks are special features that help developers hook into react state and lifecycle. It introduced a new way of writing UI components in your applications.

Before I continue any further, I will make assumptions about what you are required to know to get the most out of this article.

PREREQUISITES

  • Familiarity with HTML, CSS and JavaScript.

  • Familiarity with ES6 syntax.

  • Familiarity with React and React Hooks.

React Hooks gives a lot of power to the developer, and as the saying goes,

great power comes with a lot of responsibility

Hence this article will provide you with a suite of best practices that you can follow when working with react and react hooks.

What's in it for me?

It is not mandatory to adopt what I introduce in this article, however, doing so comes with some benefits which include;

  • Code maintainability.

    Even if you are working on a "toy" project, following best practices makes your code readable and understandable when you ignore your toy project and come back to it after some months, thereby it's very crucial to write programs that are maintainable.

  • Scalability.

    Following best practices prevents bad code, and the absence of bad code prevents bugs which makes your program scalable.

Now that you know the benefits of following best practices, let's take a look at the key best practices this article will focus on which are centered around the most widely used hooks in react projects.

  1. General Rules of Hooks.

    • Hooks can only be called inside of react function components.

    • Hooks can only be called at the top level of a component.

    • Hooks cannot be called conditionally.

  2. useState best practices.

    • Make use of descriptive state variable names.

    • State as an object-when state variables are related.

    • Using functions to update state.

    • Avoid complex logic in state initialization.

  3. useEffect best practices.

    • A critical look at dependency array.

    • Using cleanup function.

    • Keep effect logic simple.

  4. useCallback best practices.

    • Avoid unnecessary memoization.

    • Avoid passing unnecessary dependencies.

The state and effect hooks.

There's no doubt that the most used hooks in React are the state and effect hooks, hence I'll give a basic explanation to these hooks for the purpose of this article. Kindly check out the official react documentation for further information.

useState

State is the internal value of a component. This is a value that a component keeps track of during its lifecycle. The useState hook is used to hold a value and helps a component keep track of it during its lifecycle. Below is a snippet showing how useState hook can be used to hold user age information.

import { useState } from "react"

export default function User() {
const [age, setAge] = useState("")
return (...)
}

useEffect

useEffect hook is used for handling side effects - anything that is not concerned with the evaluation and rendering of react components. i.e. data fetching from an API, accessing local storage etc. The snippet below shows how this hook is used to fetch user information from an API.

import { useEffect, useState } from "react"

export default function User()  {
const [user, setUser] = useState("")

useEffect(() => {
    fetch(url)
        .then((res) => res.json())
        .then((data) => setUser(data))
}, [])
return (...)
}

is it worth it?

Yes !. Using hooks to write stateful components with functions is much easier and leaner as compared to writing them in class components. For instance, say we have a fictional movie application that fetches a list of movies from an API, let's look at the implementation with function and class components.

import React, { Component } from "react";

class MovieApp extends Component {
  constructor() {
    super();
    this.state = {
      movies: [],
      isLoading: false,
      error: null,
    };
  }

  componentDidMount() {
    const API_URL = "https://example.com/api/movies";
    this.setState({ isLoading: true });

    fetch(API_URL)
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error("Failed to fetch data");
        }
      })
      .then((data) => this.setState({ movies: data, isLoading: false }))
      .catch((error) => this.setState({ error, isLoading: false }));
  }

  render() {
    const { movies, isLoading, error } = this.state;

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

    return (
      <div>
        <h1>Movie List</h1>
        <ul>
          {movies.map((movie) => (
            <li key={movie.id}>{movie.title}</li>
          ))}
        </ul>
      </div>
    );
  }
}

export default MovieApp;

Similar implementation but with function;

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

export default function MovieApp() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const API_URL = "https://example.com/api/movies";
    fetch(API_URL)
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error("Failed to fetch data");
        }
      })
      .then((data) => {
        setMovies(data);
        setIsLoading(false);
      })
      .catch((error) => {
        setError(error);
        setIsLoading(false);
      });
  }, []);

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

  return (
    <div>
      <h1>Movie List</h1>
      <ul>
        {movies.map((movie) => (
          <li key={movie.id}>{movie.title}</li>
        ))}
      </ul>
    </div>
  );
}

From both code snippets, we can testify that we write less code when using hooks with functional components as compared to class components.

The following are the reasons why you should use react hooks;

  1. Using hooks makes your code readable and leaner.

  2. The learning curve for hooks is easier compared to classes.

  3. You tend to write more reusable components.

General Rules of Hooks.

  1. Hooks can only be called inside of react function components.

    React functional components are javaScript functions that return JSX - the HTML-like code you write. React requires that you call hooks in such functions.

  2. Hooks can only be called at the top level of components.

    React relies on the order in which hooks are called, by calling them at the top level you ensure that the execution order on every render is consistent. React needs this to correctly update and synchronize components' state and effects.

  3. Hooks cannot be called conditionally.

     function Visible() {
         const [isVisible, setIsVisible] = useState(false)
    
         if(isVisible) {
             const [count, setCount] = useState(0)    
         }
         ...
     }
    

    I mentioned that react relies on the order in which hooks are called to correctly update and synchronize component states and effects. If this rule is violated as seen from the code snippet above, your application will not behave as you expect.

    From the snippet above, the count state is not initialized in the first render, however, say the isVisible changes to true, the count state is then initialized, because the initialization of the count state is not consistent across every render, unexpected bugs will occur.

useState Best Practices.

  1. Make use of descriptive state variable names.

    Say you are building an e-commerce application, and you have a cart component that keeps track of the number of cart items in the cart, let's compare the two code snippets below;

    Listing 1

     import { useState } from "react"
    
     export default function Cart() {
         const [qty, setQty] = useState(0)
    
         return (...)
     }
    

    Listing 2

     import { useState } from "react"
    
     export default function Cart() {
         const [cartQuantity, setCartQuantity] = useState(0)
    
         return (...)
     }
    

    It may look like an over-exaggerated example, but the distinguishing factor between the two snippets is how easily a developer can skim through the code and know exactly what the code is trying to achieve. It makes your code readable.

  2. State as an object.

    React doesn't prevent you from adding thirty state variables in one component, why would you do that?

    Whether those state variables are related or not, in the name of clean code and best practices, it is much preferable to group related state variables together as object.

    You might be building an application that requires user information, one way is to write it like this;

     import { useState } from "react"
    
     export default function User() {
         const [firstName, setFirstName] = useState("")
         const [lastName, setLastName] = useState("")
         const [country, setCountry] = usState("")
    
         return (...)
     }
    

    another more elegant way is this;

     import { useState } from "react"
    
     export default function User() {
         const [user, setUser] = useState({
             firstName: "",
             lastName: "",
             country: ""
         })
    
         return (...)
     }
    

    Both approaches work, however, the latter reduces the number of state calls which can improve performance and it is much cleaner.

  3. Using functions to update state.

    Using functions to update state is recommended because of the following;

    • Ensures correctness of state updates.

      Using an updating function in setCount ensures that you get the latest state when updating it and prevents race conditions.

        import { useState } from "react"
      
        export default function Counter() {
            const [count, setCount] = useState(0)
      
            const onIncrement = () => {
                setCount(prevCount => prevCount + 1)
            }
      
            return (...)
        }
      
    • Prevent stale closures.

      When functions are used to update the state, it capture the current state correctly, avoiding issues with stale closures which occur when state is modified directly.

  4. Avoid complex logic in state initializations.

     import { useState } from "react"
    
     export default function ComplexInitialization() {
         const [userInfo, setUserInfo] = useState(getUserInfoFromServer())
    
         return (...)
     }
    

    The hook initialization above involves fetching user information from a server which is complex and time-consuming, this makes it difficult to test. This logic should be removed into an effect hook to separate concerns.

useEffect Best Practices.

  1. A critical look at dependency array.

    It is crucial to be cautious of what values you pass inside the dependency array as this can heavily affect performance. let's take a look at the code snippet below;

     import { useEffect, useState } from "react"
    
     export default function App() {
         const [movieDetail, setMovieDetail] = useState({})
    
         useEffect(() => {
             fetchMovieDetail(movieId)        
         }, [movieId])
    
         return (...)
     }
    

    When an empty dependency array is passed to the useEffect hook, it tells react to run the code once, however, if you want react to execute the hook again, you pass in a value to the array, and whenever the value changes, react run the effect function again.

    From the snippet above, whenever the movieId changes, react re-run the code in the useEffect hook.

    You should include any value that the effect function depends on in the dependency array.

  2. Using Cleanup function.

    A cleanup function is executed when the component unmounts. We use cleanup function to unsubscribe from event listeners, cancel network requests or clear intervals.

    The snippet below shows how a cleanup function can be used to clear timer intervals.

     useEffect(() => {
       const timerId = setInterval(() => {
         console.log("cleanup...")
       }, 1000);
    
       return () => {
         clearInterval(timerId);
       };
     }, []);
    
  3. Keep effect logic simple

    If you find yourself trying to handle multiple unrelated tasks in a single effect hook, you can follow the Single Responsibility Principle Where you split your effect logic into multiple effect functions.

     useEffect(() => {
       // Fetch data
     }, [dataDependency]);
    
     useEffect(() => {
       // Update the UI
     }, [data]);
    

useCallback Best Practices.

  1. Avoid unnecessary memoization.

    In scenarios where a function definition is passed to a child component from a parent as a prop, useCallback can be used to memoize the function to prevent the child component from re-rendering whenever the parent component updates.

    Note that you just might not need to use useCallback in any other scenario.

    To think that using alot of useCallback will increase the performance of your application because it's a performance hook will do more harm than good.

    The reason is, that each useCallback function call executes a set of code that consumes some amount of memory, and with a lot of unnecessary useCallback calls will consume more memory which can hurt your application performance.

  2. Avoid passing unnecessary dependencies.

    useCallback relies on dependencies to know when to re-calculate the cache function if need be. Whenever these dependencies change, the function is re-calculated and the result is cached.

    Thereby you should pass an array of dependencies as the second argument when using useCallback hook.

CONCLUSION

This article covered basic best practices that can help you improve as a React developer.

To follow best practices is your choice and comes with it's pros and cons.

REFERENCES

  1. Official React Documentation

  2. Log rocket : React Hooks

  3. React, wikipedia