Is it possible to place useEffect inside a if statement?

Viewed 2317

So guys, I've been working on my Landing and Dashboard page.

So workflow of the page is this: User gets on Landing page where he can choose to insert into form location, or press a button to recive all locations. Now on the backend I've made two APIs one to get all locations, and second where I've added :location as req.body.param and then filter locations based on that param. And everything works fine in postman.

Now because I've got two ways of user getting locations(all of them or some that he wants) I've thinked that I place two useEffects inside if statement like this:

const filter = props.location.data;
if (filter) {
    useEffect(() => {
      const fetchFiltered = async () => {
        const res = await ArticleService.filterByName(filter);
        setContent(res.data);
      };

      fetchFiltered();
    }, []);
  } else {
    useEffect(() => {
      const fetchPosts = async () => {
        const res = await ArticleService.articles();
        setContent(res.data);
      };

      fetchPosts();
    }, []);
  }

So my logic behind this was if there is filter inside props.location execute me useEffect which gets data from ArticleService who then send filter inside of a api url. If there is no filter just retrieve me all data, and setContent to res.data.

But when I compiled the code error is this: React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render

Is there some way of doing this with my logic or I need to create two components: one normal dashboard and second for filtered result?

Landing.js where user sends location

<Form>
   <div className='form-group'>
      <Input
       type='text'
       className='form-control text-center'
       name='name'
       placeholder='Enter desired location'
       value={location}
       onChange={onChangeLocation}
       />
      <Link to={{ pathname: '/dashboard', data: location }}>
         <i className='fas fa-check'></i>
      </Link>
   </div>

    <p className='text-center'>or</p>
    <Link className='btn btn-primary btn-block' to='/dashboard'>
       Show all locations
    </Link> 
</Form>

Dashboard.js

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';

import Pagination from 'react-js-pagination';

import ArticleService from '../../services/article.service';

const Dashboard = (props) => {
  const [content, setContent] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [postsPerPage] = useState(10);

  const filter = props.location.data;

  if (filter) {
    useEffect(() => {
      const fetchFiltered = async () => {
        const res = await ArticleService.filterByName(filter);
        setContent(res.data);
      };

      fetchFiltered();
    }, []);
  } else {
    useEffect(() => {
      const fetchPosts = async () => {
        const res = await ArticleService.articles();
        setContent(res.data);
      };

      fetchPosts();
    }, []);
  }

  let counter = content.length;

  // Get current posts
  const indexOfLastPost = currentPage * postsPerPage;
  const indexOfFirstPost = indexOfLastPost - postsPerPage;
  const currentPosts = content.slice(indexOfFirstPost, indexOfLastPost);

  // Change page
  const handlePageChange = (pageNumber) => {
    setCurrentPage(pageNumber);
  };

  const render = (item, index) => {
    return (
      <tr key={index}>
        <td className='text-center'>
          <div key={item.id}>
            <img
              src={`${item.pictures}`}
              alt='slika artikla'
              className='rounded'
            ></img>
          </div>
        </td>
        <td className='text-center'>
          <div key={item.id}>
            <h4>{item.descr}</h4>
            <br></br>
            <h6 className='text-left'>Number of m2: {item.sqm}m2</h6>
            <div className='text-left'>
              <small className='text-left'>
                {' '}
                <a href={item.link} target='_blank' rel='noopener noreferrer'>
                  Show on website
                </a>
              </small>
            </div>
          </div>
        </td>
        <td className='text-center'>
          <div key={item.id}>
            <h4>{item.price}</h4>
            <small className='text-left'>Price per m2: {item.ppm2}</small>
          </div>
        </td>
        <td className='text-center'>
          <div key={item.id}>
            <Link to={`/article/${item.id}`}>
              <h4>Show</h4>
            </Link>
          </div>
        </td>
      </tr>
    );
  };

  return (
    <div>
      <div className='container'>
        <h4 className='text-center'>
          Number {counter}
        </h4>
        <div className='table-responsive'>
          <table className='table'>
            <thead className='thead-dark'>
              <tr>
                <th className='text-center' scope='col'>
                  Pic
                </th>
                <th className='text-center' scope='col'>
                  Description
                </th>
                <th className='text-center w-25' scope='col'>
                  Price
                </th>
                <th className='text-center' scope='col'>
                  Show offer
                </th>
              </tr>
            </thead>
            <tbody>{currentPosts.map(render)}</tbody>
          </table>
        </div>
      </div>
      <nav>
        <div className='w3-bar w3-xlarge'>
          <ul className='pagination justify-content-center'>
            <li className='page-item'>
              <Pagination
                hideDisabled
                hideNavigation
                hideFirstLastPages
                currentPage={currentPage}
                itemsCountPerPage={10}
                totalItemsCount={content.length}
                pageRangeDisplayed={indexOfLastPost}
                onChange={handlePageChange}
              />
            </li>
          </ul>
        </div>
      </nav>
    </div>
  );
};

export default Dashboard;

Thanks! :D

2 Answers

Basic answer, no, you cannot conditionally call useEffect. You must put the conditional logic inside the useEffect callback.

const filter = props.location.data
useEffect(() => {
  if (filter) {
    const fetchFiltered = async () => {
      const res = await ArticleService.filterByName(filter)
      setContent(res.data)
    }

    fetchFiltered()
  } else {
    const fetchPosts = async () => {
      const res = await ArticleService.articles()
      setContent(res.data)
    }

    fetchPosts()
  }
}, [filter, setContent, ArticleService.filterByName, ArticleService.articles])

Hooks in React do not really follow the standard rules of javascript. There are performance reasons around the way they have to be implemented, often some caching is done to stop excess code being executed every time a render pass is done.

The useEffect hook will only run its callback function during a render where one of the values in the dependency array (the second arg of useEffect) has changed. It's standard to put in all external values that could change. Thus when the value of filter changes, the app will rerender, the useEffect will do a comparision, realise that something has changed and run it's call back again which will then perform the if statement.

You can read more about this in the performance part of the docs

In addition to the above answer,from the official documentatation

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

Related