DEV Community

Cover image for React Hooks Fundamentals for Beginners: How to Use useState and useEffect hooks 2023
Chidera Humphrey
Chidera Humphrey

Posted on • Edited on

React Hooks Fundamentals for Beginners: How to Use useState and useEffect hooks 2023

Table of contents


What are react hooks

React hooks are functions that let you use different React features from your functional components. This means that you can manage state, make API calls, and other things a class-based component can do from functional components.

There are different hooks in React which let you do different things.

  • useState hook lets you create and manage state.
  • useEffect hook lets you handle side-effects in your React application
  • useContext hook lets you share state without prop drilling, awesome right?
  • useMemo hook: lets you memoize your react components
  • useCallback hook is used to memoize functions, optimizing performance by preventing unnecessary re-rendering of components that depend on those functions.
  • useReducer hook: this hook is similar to the useState hook but handles complex state more efficiently.
  • useRef hook provides a way to create mutable references to elements or values that persist across renders.

This is not an exhaustive list but these are the hooks you will use 99% of the time.

Managing state with useState hook

The useState hook lets you add and manage state in a React component. It’s equivalent to this.state = {} in class components.

To use the useState hook, call it at the top-level of your component:

const [state, setState] = useState(initialState);

Enter fullscreen mode Exit fullscreen mode

It returns an array of exactly two values: current state and a function for updating that state.

Let’s look at them closely.

state: this is the current state. When your component first renders, the state equals the initialState.

setState: this is a function for updating the state and trigger a re-render.

initialState: the value you want the state to be initially. It can be of any valid JavaScript value.

Note: The setState function only updates the state variable for the next render. If you read the state variable after calling the setState function, you will still get the old value that was on the screen before your call.

Also, React will skip re-rendering of your component if the value you provide is identical to the current state.

Usage

To use the useState hook, you call it at the top level of your component.

import {useState} from react
   function (){
const [state, setState] = useState(‘’);
}
Enter fullscreen mode Exit fullscreen mode

Basic applications of useState

Updating a text field

In this simple example, the firstName and lastName state variables hold a string. When you type into the input fields, the handleFirstName and handleLastName updater functions read your inputs in the DOM and updates the UI accordingly.

import { useState } from 'react';

export default function MyInput() {
  const [firstName, setFirstName] = useState(‘’);
  const [lastName, setLastName] = useState(‘’);

  function handleFirstName(e) {
    setFirstName(e.target.value);
  }
function handleLastName(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <input value={firstName} onChange={handleFirstName} />
     <input value={lastName} onChange={handleLastName} />
      <p>{firstName} {lastName} is typing.</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Updating objects and arrays in state

When updating arrays or objects in state, you should not directly mutate the state.

Here’s what I mean 👇

state.age = 42; // you should not directly mutate state.
Enter fullscreen mode Exit fullscreen mode

Instead, you should replace the old state with a new one.

SetState({
...old state, 
age: 42
})
Enter fullscreen mode Exit fullscreen mode

This is also applicable to arrays.

state.push(Banana) // not recommended 
     setState([...prevState, Banana])
Enter fullscreen mode Exit fullscreen mode

Example of object in state

In this example, the employee state variable holds an object. Each input field has a change handler that calls the setEmployee function to update the state. The spread syntax (...) to make sure state is replaced and not mutated.

import { useState } from 'react';

export default function Employee() {
  const [employee, setEmployee] = useState({
    firstName: “”,
    lastName: “”,
    job: '',
  });

  return (
    <>
      <label>
        First name:
        <input
          value={employee.firstName}
          onChange={e => {
            setEmployee({
              ...employee,
              firstName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Last name:
        <input
          value={employee.lastName}
          onChange={e => {
            setEmployee({
              ...employee,
              lastName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Job:
        <input
          value={employee.job}
          onChange={e => {
            setEmployee({
              ...employee,
              job: e.target.value
            });
          }}
        />
      </label>
      <p>
        {employee.firstName}{' '}
        {employee.lastName}{' '}
        ({employee.job})
      </p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Example of array in state

In this example, the fruits state variable holds an array. Each button handler that calls the setFruits function to update the state. The spread syntax (...) to make sure state is replaced and not mutated.

import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';

let nextId = 3;
const initialTodos = [
  { id: 0, title: "'Buy milk', done: true },"
  { id: 1, title: "'Eat tacos', done: false },"
  { id: 2, title: "'Brew tea', done: false },"
];

export default function TaskApp() {
  const [todos, setTodos] = useState(initialTodos);

  function handleAddTodo(title) {
    setTodos([
      ...todos,
      {
        id: nextId++,
        title: "title,"
        done: false
      }
    ]);
  }

  function handleChangeTodo(nextTodo) {
    setTodos(todos.map(t => {
      if (t.id === nextTodo.id) {
        return nextTodo;
      } else {
        return t;
      }
    }));
  }

  function handleDeleteTodo(todoId) {
    setTodos(
      todos.filter(t => t.id !== todoId)
    );
  }

  return (
    <>
      <AddTodo
        onAddTodo={handleAddTodo}
      />
      <TaskList
        todos={todos}
        onChangeTodo={handleChangeTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Handling side effects with useEffect hook

The useEffect hook is used to handle side effects in React. Side effects mean operations not controlled by React. Common side effects include:

  • Making calls to a third-party API
  • Interacting with the browser’s local storage
  • Communicating with the DOM
  • Implementing certain libraries such as: animation libraries.

To use the useEffect hook, declare it at the top-level of your component

import React, { useEffect } from 'react';

function ExampleComponent() {
  useEffect(() => {
    // Side effect code goes here

    // Clean up the effect (optional)
    return () => {
      // Cleanup code goes here
    };
  }, []);

  return (
    // Component JSX goes here
  );
}


export default ExampleComponent;
Enter fullscreen mode Exit fullscreen mode

The useEffect hook takes two arguments:

The first argument is a callback function that contains the code for the side effect. This is where you can perform API calls, manipulate the DOM, interact with browser storage, or integrate with external libraries.

The second argument is the dependency array, which determines when the effect should run. By passing an empty array [], the effect will only run once, after the initial render.
If you want the effect to run again when certain dependencies change, you can include them in the dependency array.

Now that you have a good understanding of the useEffect hook and its purpose in handling side effects in React, let's look at some common use cases and see how it can be used in practice.

Making calls to third-party APIs

In this example, we'll look at how to use the useEffect hook in React to make calls to third-party APIs. Making API calls is something you will do more often as a front-end developer, and the useEffect hook provides a convenient way to handle such asynchronous operations.

We are going to use the IMDB API to retrieve details of a film.
Let's dive into the code to see how it's done.

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

function FilmDetails() {
  const [film, setFilm] = useState(null);

  useEffect(() => {
    // Function to fetch film details from the IMDB API
    const fetchFilmDetails = async () => {
      try {
        const response = await fetch('https://api.example.com/films/123');
        const data = await response.json();
        setFilm(data);
      } catch (error) {
        console.error('Error fetching film details:', error);
      }
    };

    fetchFilmDetails();

    // Cleanup function
    return () => {
      // Cleanup code goes here
      // This will be executed when the component is unmounted or when the dependency changes
    };
  }, []);

  if (!film) {
    return <p>Loading film details...</p>;
  }

  return (
    <div>
      <h2>{film.title}</h2>
      <p>{film.description}</p>
      {/* Render other film details */}
    </div>
  );
}

export default FilmDetails;

Enter fullscreen mode Exit fullscreen mode

In this example, we have a functional component, FilmDetails. We also use the useState hook to manage the film state which is initially set to null.
Inside the useEffect hook, we define an asynchronous function to fetch data from the IMDB API.

If the API call succeeds, we update the film state with the retrieved data using setFilm.

If an error occurs during the API call, we log the error message to the console within the catch block.

We passed an empty dependency array as the second argument. This means that the effect will run only once—on initial render.

For better user experience, we have added a loading state in case the film data is yet available. Once it is available, we render the component JSX.

Finally, the return statement inside the useEffect returns a clean-up function, which will be executed when the component is unmounted or when the dependency changes. You can add any necessary clean-up code, such as canceling ongoing requests or removing event listeners, inside the clean-up function.

If the API call was successful, you should see the fetched film details displayed in your browser.

Interacting with the browser's local storage

In this example, we are going to reading and writing to the browser's local storage.
The browser's local storage is used for persisting data.

Let's dive into the code.

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

function LocalStorageExample() {
  const [name, setName] = useState('');

  useEffect(() => {
    // Read from local storage
    const storedName = localStorage.getItem('name');
    if (storedName) {
      setName(storedName);
    }
  }, []);

  const handleNameChange = (e) => {
    const newName = e.target.value;
    setName(newName);

    // Write to local storage
    localStorage.setItem('name', newName);
  };

  return (
    <div>
      <h2>Hello, {name || 'stranger'}!</h2>
      <input type="text" value={name} onChange={handleNameChange} />
    </div>
  );
}

export default LocalStorageExample;
Enter fullscreen mode Exit fullscreen mode

In this example, we define a functional component, LocalStorageExample. We use the useState hook to manage the name state.

We use the useEffect hook to interact with the local storage. First, we read from the local storage using the getItem() method. If name key exists, we set the name state to the value using the setName function.
Next, we define a handleNameChange function that is triggered when the input's value changes. It updates the name state with the new value and uses localStorage.setItem() to write the new value to the local storage, associating it with the key 'name'.

The component renders a heading that displays the greeting with the current name (or stranger if no name is set), and an input field where the user can enter a new name.

Note: This code will not work in server-side as the localStorage only exists in the browser.

Refactoring Class Components to Functional Components with Hooks

Hooks were only introduced in React 16. This means that if you have been using React before then, you will most likely be using class components for tracking and managing data, as well as fetching and sending data.

In this section, I am going to show you how to refactor your class components to functional components using hooks.

Refactoring State Management: From Class Components to Functional Components

To refactor class components with state to functional components with state, we convert the class component's state management using this.state and this.setState to functional components utilizing the useState hook.

Class component

import React, { Component } from 'react';

class ClassComponentWithState extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
}

export default ClassComponentWithState;
Enter fullscreen mode Exit fullscreen mode

Corresponding functional component

import React, { useState } from 'react';

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

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

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

export default FunctionalComponentWithState;
Enter fullscreen mode Exit fullscreen mode

We converted the class component to functional component by replacing this.state and this.setState with the useState hook. As explained above, the useState hook returns an array with exactly two values: current state and an updated function.

Refactoring Data Fetching: From Class Components to Functional Components

To refactor data fetching from class components to functional components, we convert the class component's approach, which typically uses lifecycle methods like componentDidMount and componentDidUpdate, to functional components utilizing the useEffect hook for handling data fetching and updating component state.

Class component with data fetching

import React, { Component } from 'react';

class ClassComponentWithDataFetching extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: [],
      updateCount: 0,
    };
  }

  componentDidMount() {
    // Data fetching
    this.fetchData();
  }

  componentDidUpdate(prevProps, prevState) {
    // Check if updateCount has changed
    if (prevState.updateCount !== this.state.updateCount) {
      this.fetchData();
    }
  }

  fetchData = () => {
    // Data fetching
    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((jsonData) => this.setState({ data: jsonData }))
      .catch((error) => console.error('Error fetching data:', error));
  };

  handleUpdate = () => {
    this.setState((prevState) => ({
      updateCount: prevState.updateCount + 1,
    }));
  };

  render() {
    return (
      <div>
        {this.state.data.map((item) => (
          <p key={item.id}>{item.name}</p>
        ))}
        <button onClick={this.handleUpdate}>Update</button>
      </div>
    );
  }
}

export default ClassComponentWithDataFetching
Enter fullscreen mode Exit fullscreen mode

Functional component with data fetching

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

function FunctionalComponentWithDataFetching() {
  const [data, setData] = useState([]);
  const [updateCount, setUpdateCount] = useState(0);

  useEffect(() => {
    // Data fetching
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const jsonData = await response.json();
        setData(jsonData);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, [updateCount]); // Update when updateCount changes

  // Simulating component update triggering the useEffect
  const handleUpdate = () => {
    setUpdateCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      {data.map((item) => (
        <p key={item.id}>{item.name}</p>
      ))}
      <button onClick={handleUpdate}>Update</button>
    </div>
  );
}

export default FunctionalComponentWithDataFetching;

Enter fullscreen mode Exit fullscreen mode

We converted the class component by replacing the componentDidMount andcomponentDidUpdatewith the useEffect hook. The dependency array helps us mimick the two lifecycle methods.

Benefits of Using the React hooks

Using React hooks provides several benefits in developing React applications:

  1. Simplicity
    Hooks enable functional components to have their own state and lifecycle methods. This makes it easier to read, write, and understand code without dealing with the complexity of class components.

  2. Reusability
    Hooks promote code reuse. Custom hooks allow you to encapsulate logic and stateful behavior into reusable functions that you used across multiple components in your application. This promotes modular and maintainable code.

  3. Enhanced Functional Components
    Hooks enable functional components to have state and side effects, previously only available in class components. You can manage component state using the useState hook and handle side effects with the useEffect hook, making functional components more powerful and versatile.

  4. Improved Performance
    Hooks optimize performance by reducing unnecessary re-renders. With the useMemo and useCallback hooks, you can memoize values and functions respectively, preventing unnecessary computations or re-rendering of components.

  5. Easier Testing
    Hooks promote functional programming concepts which makes testing easier, as the logic is separated into small, testable functions. This allows you to test components in isolation.

  6. Migration from Class Components
    Hooks simplify the process of migrating from class components to functional components. Existing class components can be refactored to functional components using hooks, retaining the functionality while improving code readability and maintainability.

  7. Community Support
    Hooks have gained widespread adoption and support from the React community. This means that you can find ample resources, documentation, and community-driven libraries to assist in learning and using hooks effectively.


Concluding Thoughts

React hooks are a powerful feature that allows developers to utilize different React functionalities within functional components. They enable state management, handling side effects, and refactoring class components to functional components.

The useState hook is used for managing state, while the useEffect hook is used for handling side effects such as making API calls. Hooks provide benefits such as code simplicity, reusability, and eliminating the need for class components.

Overall, React hooks offer an efficient and effective way to enhance the functionality and development experience in React applications.

Follow me on Twitter and LinkedIn for more content and updates.

Top comments (0)