Understanding React useState and useEffect Hooks For Beginners.

React.js is by far the most popular front-end javascript library out there, almost every developer transitioning into or learning web development has made an encounter with React, and possibly decide to use it as their primary front-end library. But still, some concepts of React.js are misunderstood by new developers learning the framework, and these concepts are React Hooks.

Initially React has two component types, Class components, and Functional components. Class components use the JavaScript Class method to create components and this component can have access to state and other React features like the LifeCycle methods (componentDidMount(), componentWillUnmount(), componentDidUpdate()). But the Functional component has no access to states and the LifeCycle methods. Well, not anymore, because they do now. Hooks were added to React in version 16.8, which allows them to have access to states and other features.

In the Functional component, the useState hook is used to store and update a state. Now you might be familiar with the useState hook and how it stores and update a state, but allow me to explain further.

To use the useState hook, we import it from 'react';

import { useState } from 'react'

And we use it in our Functional component like this.

import { useState } from 'react'

const App = () => {
      const [state, setState] = useState(0)

      return <button onClick={() => setState(state + 1)}>{state}</button>
}

As a beginner, you might think initially this is how the useState hook works, we create items in an array and assign the useState hook to them.

Well, what we did there is called Destructuring.

Destructuring is a JavaScript expression that allows us to extract data from a JavaScript object or array and then set them into new distinct variables.

Or we can define it as a way of breaking down a structure (an object or an array) into simpler parts that we can assign to a variable.

The useState hook is a function that returns an array with two items, the first item is a data (a data we initially passed to it as an argument and undefined if we passed nothing) and the second item is a function that can be used to update the first item. And also yes, we can use the react useState hook like this:

import { useState } from 'react'

const App = () => {
      const state = useState(0)

      return <button onClick={() => state[1](state[0] + 1)}>{state[0]}</button>
}

But should you? No, the purpose of this article is to explain the useState hook and why we use it the way we do (by destructuring it), not to deviate you from using the standard approach.

This approach makes your code looks complicated, while the destructured approach makes it looks clean and easy and also very straightforward to use.

Now another react hook that was introduced to the Functional component was the useEffect hook.

In the class component, we have the LifeCycle methods, that we can use to listen to event changes, like when our component gets updated, when our component is about to mount, and so on.

But in the Functional component, we have the useEffect hook which pretty much does all the functions of the lifecycle methods.

The useEffect hook can be used like this:

import { useState, useEffect } from 'react'

const App = () => {
      const [state, setState] = useState(0)

      useEffect(() => {
              console.log('component is updating')
      })

      return <button onClick={() => setState(state + 1)}>{state}</button>
}

The useEffect hook accepts a function as an argument and optionally an array of state variables.

The useEffect hook will execute any function passed to it as an argument any time a state changes or the component gets updated, and if an array of state variables is also passed to it, it will execute the function only when these state variables changed.

But you can also use the useEffect hook like this;

import { useState, useEffect } from 'react'

const App = () => {
      const [state, setState] = useState(0)

      const func = () => {
              console.log('component is updating')
      }

      useEffect(func)

      return <button onClick={() => setState(state + 1)}>{state}</button>
}

Well, basically we are doing the same thing as before, but this approach is just to explain to you how the useEffect works and what it takes as arguments. Using the above example is not a good practice, because the function func will get re-created on every re-render.

useEffect hooks have what is called dependencies, these are basically the array of state variables we mentioned earlier, for example, if we want to execute a particular function after a state variable has been updated then we can pass the state variable as the useEffect dependency.

import { useState, useEffect } from 'react'

const App = () => {
      const [state, setState] = useState(0)


      useEffect(() => {
              console.log('state is updated')
      }, [state])

      return <button onClick={() => setState(state + 1)}>{state}</button>
}

the function passed as an argument to the useEffect will only be executed when the state state variable changes.

So what if we have two state variables?

import { useState, useEffect } from 'react'

const App = () => {
      const [state, setState] = useState(0)
      const [name, setName] = useState('')


      useEffect(() => {
              console.log('state is updated')
      }, [state])

      return <>
      <input onChange={(e) => setName(e.target.value)} value={name} />
      <button onClick={() => setState(state + 1)}>{state}</button>
    </>
}

We have an extra state variable called name which will be updated by the input element any time a user types into it.

But still, yet, any time you type into the input element, the useEffect will not execute the function because the state that is being updated was not passed to it in the array of dependencies.

So what if we just want to execute a function once, when the component is initially rendered, let's say we want to make an API call the first time a component is rendered on the screen and then never execute that function again, even though the states keep changing?

To do that, we pass an empty array as the second argument, which tells the useEffect hook that there is no dependencies which means it will not execute the function any time a state is updated, but only once when the component is initially rendered.

const App = () => {
      const [state, setState] = useState(0)
      const [name, setName] = useState('')

      useEffect(() => {
              //make your API call here
              //it will only be called once.
      }, [])

      return <>
          <input onChange={(e) => setName(e.target.value)} value={name} />
          <button onClick={() => setState(state + 1)}>{state}</button>
    </>
}

You can have more than one useEffect and useState hook in a component, and that is totally okay. But in situations where it is getting difficult to maintain your hooks in a component then it is good to have a custom hook, which should manage the hooks for you without messing up your component.

Did you find this article valuable?

Support Usman Gurowa Hassan by becoming a sponsor. Any amount is appreciated!