September 22, 2022

React useEffect Hook With Examples

In this tutorial you'll learn about useEffect() hook in React which performs side-effects in functional components.

What is a side effect

Side effect is some task that you want to separate from rendering. Some examples of what can be termed as side effect are-

  1. Send Http Requests for storing or fetching data to Backend servers
  2. Manipulating DOM directly
  3. Setting and managing timer functions

Syntax of useEffect

useEffect(callback, [dependencies]);

The useEffect hook takes two arguments.

First argument- A callback function containing the side-effect logic. It should be executed after component rendering, if the specified dependencies (second parameter) changes.

Second argument- Dependencies of this effect which is an optional argument and passed as an array. The useEffect hook executes the callback function (first parameter) only if the dependencies changed.

When does useEffect run

When does useEffect run depends on the dependencies passed.

  1. If no dependencies are passed the side-effect is executed after every rendering.
    useEffect(() => {
      // callback logic
    });
    
  2. If an empty array is passed as dependency then the side-effect runs only once after initial render.
    useEffect(() => {
      // callback logic
    }, []);
    
  3. When dependency array has state or props passed with in the array then the side-effect runs after initial render and then only when the dependency value changes.
    useEffect(() => {
      // callback logic
    }, [prop, state]);
    

useEffect Example

Here is a simple useEffect React hook example where we have a counter stored in a state. Every click of button increments the counter by 1 (changes the state).

Every time value of counter changes we also want to log the current counter value. This logging logic should be implemented as a side-effect.

Since execution of side-effect depends on the counter value so the state variable should be passed as dependency in the useEffect hook.

Counter.js

import { useEffect, useState } from "react";
import { Button } from "react-bootstrap";

const Counter = () => {
    const [count, setCount] = useState(0);

    function countChangeHandler(){
        setCount(count + 1);
    };
    useEffect(() => {
        console.log("Count has been updated " + count)
    }, [count])
    return(
        <div>
            <p>You clicked {count} times</p>
            <Button variant="primary" onClick={countChangeHandler}>Click Me</Button>
        </div>
    );
   
}

export default Counter;

on running it by adding following tags in App.js

return (
    <Counter></Counter>
);
useEffect with dependency

As you can see log message is logged to show count till 4 if button is clicked 4 times.

Now if you change the useEffect to have an empty array as a dependency and then run it.

useEffect(() => {
        console.log("Count has been updated " + count)
    }, [])
React useEffect without dependency

As you can notice now even if button is clicked 4 times, message is logged only once after initial render.

Component rendering and side-effect logic are separate

Component rendering and side-effect logic should be kept separate. Any side effect logic should be written as a function with in a useEffect hook.

Performing a side-effect directly in the component may affect the component rendering. If we need to perform a side effect, it should be done after component rendering is finished.

You can think of useEffect Hook as a combination of componentDidMount, componentDidUpdate, and componentWillUnmount Component lifecycle methods.

To show how it may affect the rendering if side-effect logic is placed directly in the component we'll show the frequently used login example. There are 3 components in this example one for top header, then for login page and last for landing page.

Here are the images to show the final pages.

Login Page

Landing Page after login

TopHeader.js (Uses react-bootstrap)

import { Container, Nav, Navbar } from "react-bootstrap"
const TopHeader = (props) => {
    return (
        <Navbar bg="dark" variant="dark" expand="lg">
          <Container>
            <Navbar.Brand href="#home">Login Page</Navbar.Brand>
            <Navbar.Toggle aria-controls="basic-navbar-nav" />
            <Navbar.Collapse id="basic-navbar-nav">
              <Nav className="me-auto">
                <Nav.Link href="#admin">Admin</Nav.Link>
              </Nav>
              {props.isUserAuthenticated && (
              <Nav className="justify-content-end">
                <Nav.Item>
                  <Nav.Link onClick={props.onLogout}>Logout</Nav.Link>
                </Nav.Item>
              </Nav>
              )}
            </Navbar.Collapse>
          </Container>
        </Navbar>
    );
}
export default TopHeader;

Login.js

import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import { Card } from 'react-bootstrap';
import { useState } from 'react';

const Login = (props) => {
    const [user, setUser] = useState('');
    const userChangeHandler = (event) => {
        setUser(event.target.value);
    }
    const submitHandler =(event) => {
        event.preventDefault();
        props.onLogin(user);
    }
    return (
        <Card className="mt-3 mx-auto" style={{ width: '25rem' }}>
            <Form onSubmit={submitHandler}  className="mx-4">
                <Form.Group className="mb-3">
                <Form.Label>User Name</Form.Label>
                <Form.Control type="text" name="name" 
                    placeholder="Enter user name"  value={user}
                    onChange={userChangeHandler} />
                </Form.Group>
                <Form.Group className="mb-3">
                <Form.Label>Password</Form.Label>
                <Form.Control type="password" name="password" 
                    placeholder="Enter password"  />
                </Form.Group>
                <div className="text-center mb-3">
                    <Button variant="primary" type='submit'>Login</Button>
                </div>
            </Form>
        </Card>
    );
}
export default Login;

LandingPage.js

const LandingPage = (props) => {
    return(
    <p>User logged in, welcome to our site</p>
    );
}
export default LandingPage;

If you want to ensure that user remains logged in if you refresh the page after user has logged in then you need to store the log in state. Suppose you do it as given below in App.js without using useEffect hook.

App.js

import { useEffect, useState} from "react";
import { Fragment } from "react";
import TopHeader from "./Components/Examples/TopHeader";
import Login from "./Components/Examples/Login";
import LandingPage from "./Components/Examples/LandingPage";
function App() {

  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const loginActionHandler = (userName) => {
    // store the logged in state
    localStorage.setItem('userLoggedIn', true);
    setIsLoggedIn(true);
  }

  const logoutActionHandler = () => {
    localStorage.removeItem('userLoggedIn');
    setIsLoggedIn(false);
  }
  
  // side-effect logic Written directly
  const loggedIn = localStorage.getItem('userLoggedIn');
  if(loggedIn){
    setIsLoggedIn(true);
  }


  return (
    <Fragment>
    <TopHeader isUserAuthenticated={isLoggedIn} onLogout={logoutActionHandler}></TopHeader>
    {!isLoggedIn && <Login onLogin={loginActionHandler}></Login>}
    {isLoggedIn && <LandingPage onLogout={logoutActionHandler}></LandingPage>}
    </Fragment>
  );
};

export default App;

With this way you will get the following error for the infinite re-rendering.

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

This happens because you are changing the state again in the if condition. It triggers component rendering because of state change comes back to the same if condition and again triggers component rendering resulting in infinite loop.

if(loggedIn){
    setIsLoggedIn(true);
  }

That is why side-effect logic, which in this case is to verify whether user is already logged in while rendering should be written as a callback function with in the useEffect hook.

  useEffect(()=>{
    const loggedIn = localStorage.getItem('userLoggedIn');
    if(loggedIn){
      setIsLoggedIn(true);
    }
  }, []);

With that change Login and Logout logic should work fine, even if page is refreshed after user logs in.

cleanup function in useEffect

Some side-effects require a clean up in order to avoid memory leaks. Some examples are clear timers, close subscriptions, close sockets.

With in your useEffect hook return a function with the cleanup logic. This function is considered the side-effect cleanup function by useEffect.

useEffect(() => {
  // Side-effect logic
  ...
  return function cleanup() {
    // cleanup logic
    ...
  };
}, [dependencies]);

Here is an example when the side-effect cleanup is used. Again, we have a counter that is incremented every second, to do that setTimeout function is used. To cleanup this side-effect timer is stopped by returning a clearTimeout function.

import { useEffect, useState } from "react";

const Counter = () => {
    const [count, setCount] = useState(0);

    function countChangeHandler(){
        setCount(count + 1);
    };
    useEffect(() => {
        let timer = setTimeout(
            () => {
                setCount(count + 1);
            }, 1000);
        // cleanup
        return () => clearTimeout(timer);
    }, [count])
    return(
        <div>
            <p>Current count is {count}</p>
        </div>
    );
   
}

export default Counter;

That's all for the topic React useEffect Hook With Examples. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment