Getting Started with React Hooks: A Simple Guide to useState, useEffect, and useContext

Getting Started with React Hooks: A Simple Guide to useState, useEffect, and useContext

·

5 min read

What are Hooks in React?

In React, hooks are functions that enable the utilization of state and other React features within functional components. Hooks were integrated into React in version 16.8. Prior to the introduction of Hooks, the utilization of class components was necessary to incorporate state in a React application. This needlessly increased the complexity of the code. Hooks were introduced as a means to supplant class components. This change significantly reduced the complexity of the code.

Let's understand the three fundamental hooks in React - useState, useEffect, and useContext.

  1. useState()

    This hook let's us declare and set a state in our component.

     import { useState } from "react";
    
     const Counter = () => {    
       const [count, setCount] = useState(0);  
    
       return (  
         <div>  
           <p>Count: {count}</p>  
           <button onClick={() => setCount(count + 1)}>  
             Click me  
           </button>  
         </div>  
       );  
     }  
     export default Counter;
    

    Here's a simple Counter component demonstrating the usage of useState in React. To begin, we import useState from the React library. The useState hook returns two things: the current state (or the initial value when the component is first mounted) and a function that lets us alter the state. Notice that here, the initial value is set to 0.

    This is how our application looks initially:

    When the 'Click me' button is clicked, it triggers the setCount function through an event handler (in this case, onClick). Generally, every time the state is changed, the component is re-rendered.

    useState is what we call a State Hook. It lets a component "remember" some information.

  2. useEffect()

    The useEffect hook allows you to perform side effects in your components. In React, a side effect refers to any interaction your component has with the outside world, such as data fetching, subscriptions, manual DOM manipulations, or other imperative code.

     useEffect(() => {
         //do something
     }, [dependency_list])
    

    The above is the basic structure of useEffect. It takes in two arguments - a function and a dependency list in the form of an array (this argument is optional).

    The useEffect hook can be implemented as follows:

     useEffect(() => {
         //do something
     })
    

    In this case, since no dependency list is specified, the function inside the useEffect hook will run every time the component re-renders.

     useEffect(() => {
         //do something
     }, [])
    

    In this scenario, an empty dependency list is provided. As a result, the function inside the useEffect hook runs only on the initial render.

     useEffect(() => {
         //do something
     }, [props])
    

    Here, specific prop(s) are included in the dependency list. Consequently, the function runs once when the component is first rendered (or mounted), and subsequently, it executes again only when the props specified in the dependency list undergo changes.

    To gain a better understanding of how useEffect works, let's explore an example:

     import { useEffect, useState } from "react";
    
     const Counter = () => {    
       const [count1, setCount1] = useState(0);
       const [count2, setCount2] = useState(0);
    
       useEffect(() => {
         const timer = setTimeout(() => {
           setCount2((count2) => count2 + 1);
         }, 1000);
    
         //cleanup function.
         return () => clearTimeout(timer);
       });
    
       return (  
         <div>  
           <p>Count 1: {count1}</p>  
           <button onClick={() => setCount1(count1 + 1)}>  
             Click me 1 
           </button>
    
           <p>Count 2: {count2}</p>  
           <button onClick={() => setCount2(count2 + 1)}>  
             Click me 2 
           </button>
    
         </div>  
       );  
     }  
    
     export default Counter;
    

    In this case, count2 is incremented by 1 every 1000 milliseconds (1 second). This happens because, we haven't passed in a dependency list and the function passed in will run on every re-render. And every time the count2 is increment by 1, the state changes and the component re-renders. Thus, count2 is incremented by 1 every second.

     useEffect(() => {
         const timer = setTimeout(() => {
           setCount2((count2) => count2 + 1);
         }, 1000);
    
         return () => clearTimeout(timer);
       }, []);
    

    In this case, count2 will increment to 1 during the initial render and remains like that.

     useEffect(() => {
         const timer = setTimeout(() => {
           setCount2((count2) => count2 + 1);
         }, 1000);
    
         //cleanup function.
         return () => clearTimeout(timer);
       }, [count1]);
    

    In this case, count2 will increment to 1 in the initial render and remains like that until we change count1. When we increment count1 by clicking it's button, we'll be able to observe that count2 is also getting incremented by 1 (with a delay of 1s).

    The useEffect hook also allows you to return a cleanup function. The cleanup function in the useEffect hook is responsible for cleaning up any resources or side effects that were established during the execution of the useEffect. By including this cleanup function, we can ensure that there are no lingering timeouts when the component is no longer in the DOM, thus helping to prevent potential memory leaks or unexpected behavior.

  3. useContext()

    Before we explore the useContext() hook, it is essential to have a clear understanding of what 'Context' means in React. In React, Context provides a mechanism for passing data down a component tree. We usually pass data to a component as "props". However, when it comes to passing data through multiple levels of components, it can become cumbersome. This is called prop-drilling. To avoid prop-drilling, we can use Context (no pun intended).

     import { createContext, useContext } from "react";
    
     const SomeContext = createContext();
    
     const SomeContextProvider = ({ children }) => {
       const data = "some data passed down using context";
    
       return(
         <SomeContext.Provider value={{ data: data }}>
           { children }
         </SomeContext.Provider>
       )
     };
    
     const Child = () => {
       const context = useContext(SomeContext);
       return <div>{context.data}</div>
     };
    
     const App = () => {
       return (
         <SomeContextProvider>
           <Child />
         </SomeContextProvider>
       );
     };
    
     export default App;
    

    Here, we create a new context using 'createContext'. Then, we create a component called SomeContextProvider. This component wraps its children with SomeContext.Provider, passing down the value prop, which contains an object with the data. The Child component then uses the context we created using the useContext hook to access the data from the context. Finally, the App component is defined. It renders the SomeContextProvider component, which wraps the Child component. This structure allows the Child component to access the data provided by the context.

Thus, hooks enabled developers to incorporate state without using class components in React. The simplicity introduced by hooks, such as useState, useEffect and useContext enables a more intuitive and efficient development process. This made the code cleaner and easier to maintain.

I highly recommend checking out this highly insightful 2018 talk by the members of the core team of React to learn more about the vision behind Hooks.