React hooks: An in-depth look at useState and useEffect

React hooks are simple JavaScript functions that developers can use to isolate reusable parts of code from a functional component. They can be stateful and can manage side effects, such as the ones we're about to look at now.

At the end of this article, you should be able to:

  • describe the useState hook

  • describe the useEffect hook

  • learn the best practices for using hooks

  • understand the differences between hooks and class components

  • understand limitations of using these hooks

useState Hook

The useState hook is a hook that lets you add a state variable to your component. State in this case refers to a built-in object in React that is used to contain data or information about the component.

const [state, setState] = useState(initialState)

This is typically how the useState hook is used. The initialState refers to the starting value of the state. This could be anything ranging from a string to a number, to an object. The state also returns exactly two values, which are the current state, and the set function which updates the state to a new value, and triggers a re-render. The current state always matches the initialState during the first render. Always remember that hooks are called at the top level of your components, and as such useState hooks must always be at the top of your component, and we shall see why soon.

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("Taylor");

  return (
    <div>
      <button onClick={() => setCount((count) => count + 1)}>
        count is {count}
      </button>
      <button onClick={() => setName((name) => "Edwards")}>
        my name is {name}
      </button>
    </div>
  );
}

In the example above, the first piece of state holds the value of a number count. It starts at 0, as seen on the right-hand side of the function, and when the button is clicked, the number moves from 0 to the next number which is 1, and so on. The next piece of the hook starts with the name Taylor, but as the button is clicked, it is updated to Edwards. This is basically how the useState hook works, it holds a piece of state that you would like to update and does so accordingly when set up properly.

useEffect Hook

The useEffect hook is a hook that allows you to add side effects to a functional component. Such effects include fetching data, directly updating the DOM, running timers, etc. it usually accepts two arguments, but the second argument can be optional.

useEffect(setup, dependency)

This is typically how a useEffect hook is being used. The setup is the function with a developer's logic, that is what the developer wants the hook to do. Your setup function may also optionally return a cleanup function. When your component is first added to the DOM, React will run your setup function. After every re-render with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. After your component is removed from the DOM, React will run your cleanup function one last time. The dependency(which is optional) is the list of all reactive values referenced inside of the setup code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison. If you omit this argument, your Effect will re-run after every re-render of the component.

import { useState, useEffect } from "react";

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

  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  });

  return <h1>I've rendered {count} times!</h1>;
}

When you run this code, you notice that the hook keeps re-rendering even though it was supposed to render only once. What could be the issue? Well, when you look at the code, you notice that there is no dependency included in the hook. This is the problem with running the hook without a dependency, so it is always advised to add the dependency, which is usually an empty array, except there is a reactive value to be added to the array. When we take cognizance of this, the hook above use becomes:

import { useState, useEffect } from "react";

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

  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []); // <- add empty brackets here

  return <h1>I've rendered {count} times!</h1>;
}

Best Practises for Using Hooks

There are some best practices for using hooks. the first is to make sure to always keep hooks at the top of your component. this ensures that hooks are called in the same component each time a component is rendered. Code is evaluated from top to bottom, and as such hooks need to be at the top. That's what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. Also, conditional hooks must be avoided by all means. Conditional hooks in React should be avoided because they can cause unexpected behavior in your application. Hooks in React are designed to be called in the same order on every render. When you use a conditional hook, it can lead to the hook being called in different orders based on the condition. This can lead to bugs, such as states not being updated when they should be, or components not being re-rendered when they should be. Another reason to avoid conditional hooks is that they can make your code harder to understand and maintain. When you have conditional logic scattered throughout your code, it can be difficult to follow the flow of your application and understand what is happening. This can make it harder to make changes or fix bugs in your code. Instead of using conditional hooks, it's recommended to use conditional rendering to control when components are displayed or not. This can help keep your code clean and easy to understand, while still allowing you to control the behavior of your components based on conditions. Another best practice in React would be using custom hooks. Custom hooks allow you to extract complex logic from your components into a separate function, which can then be used across multiple components. This can help reduce code duplication and make your code easier to maintain. It can also make your components more focused on their specific responsibilities, making them easier to understand and debug. Custom hooks can also help you separate concerns in your code. Instead of having complex logic mixed in with your component's render method, you can abstract that logic into a custom hook and keep your components focused on rendering UI. This can make your code more modular and easier to reason about. Additionally, custom hooks can make your code more testable. Since custom hooks are just functions, they can be easily unit tested in isolation from your components. This can help you catch bugs earlier in the development process and ensure that your code is working as expected.

Comparisons between Hooks and Class components

Hooks and class components are two different ways of writing components in React. Hooks were introduced in React 16.8 as a new way to manage state and side effects in functional components, while class components have been the traditional way of writing components in React.

Here are some differences between hooks and class components:

  1. Syntax: Hooks use a functional syntax, while class components use a class syntax.

  2. State management: With hooks, state can be managed using the useState hook, whereas in class components, state is managed using the this.state object.

  3. Side effects: With hooks, side effects can be managed using the useEffect hook, while in class components, side effects can be managed using lifecycle methods such as componentDidMount and componentDidUpdate.

  4. Reusability: Hooks can be easily reused across multiple components, while with class components, it can be harder to reuse functionality across different components.

  5. Performance: Hooks can be more performant than class components because they avoid the overhead of creating a new class instance for each component.

Overall, hooks provide a more concise and flexible way of writing components in React, while class components provide a more familiar syntax and a long history of usage. As of React 17, hooks can be used in both functional and class components, so it's possible to gradually transition from class components to using hooks over time.

Limitations in using Hooks

Although hooks provide many benefits for writing React components, there are some limitations and considerations to keep in mind:

  1. Rules of hooks: The rules of hooks must be followed to ensure proper usage of hooks. For example, hooks can only be called at the top level of a functional component, and they must not be called conditionally or within loops.

  2. Learning curve: Hooks introduce a new way of managing state and side effects in functional components, which can have a learning curve for developers who are used to writing class components.

  3. Compatibility: Some third-party libraries and React Native may not support hooks yet, so it's important to check for compatibility before using hooks in those contexts.

  4. Refactoring existing code: Converting existing class components to functional components with hooks may require significant refactoring, depending on the complexity of the component.

  5. Debugging: Debugging can be more challenging with hooks because the state and side effects are managed differently than with class components.

  6. Dependency arrays: The useEffect hook relies on a dependency array to determine when to run, and it can be easy to accidentally leave out a dependency or include one that is unnecessary, leading to bugs.

Overall, hooks provide a powerful tool for writing React components, but it's important to be aware of the limitations and considerations before deciding to use them in a project. It's also recommended to thoroughly test and review code that uses hooks to ensure proper usage and avoid potential bugs.

Conclusion

In conclusion, the useState and useEffect hooks are powerful tools for managing state and side effects in React functional components.

The useState hook allows developers to easily manage state in functional components, eliminating the need for class components and the complexity that comes with them. By using the useState hook, developers can easily update and access state in a declarative and intuitive way.

The useEffect hook allows developers to manage side effects, such as fetching data from APIs or updating the DOM, in functional components. By using the useEffect hook, developers can ensure that these side effects are properly managed and do not cause issues in the application.

It's important to remember to follow the rules of hooks when using useState and useEffect hooks to avoid unexpected behavior. It's also important to properly test and review code that uses hooks to ensure proper usage and avoid potential bugs.

Overall, the useState and useEffect hooks are powerful and essential tools for writing clean, maintainable, and efficient React code.