-1

I am working on the ReactJS Project and in there I want a component which does something every time it renders but not every time something change in it.

Dashboard.js

import React, { useCallback, useEffect, useState } from 'react'
import Product from './Product'
import { database } from '../../firebase/firebase'

const Dashboard = () => {

    const [defaults, setDefaults] = useState([])
    const [firstKey, setFirstKey] = useState('')
    const [lastKey, setLastKey] = useState('')

    const convertToArray = (data) => {
        const tempArray = []
        for (let product in data) {
            tempArray.push({
                id: product,
                amount: data[product]['amount'],
                productName: data[product]['productName']
            })
        }
        return tempArray
    }

    const nextButtonListener = async () => {
        const nextData = await (await database.ref('/system').orderByKey().startAfter(lastKey).limitToFirst(10).once('value')).val()

        const keys = Object.keys(nextData)

        setFirstKey(keys[0])
        setLastKey(keys[keys.length - 1])
        setDefaults(oldDefaults => convertToArray(nextData))
    }

    const previousButtonListener = async () => {
        const previousData = await (await database.ref('/system').orderByKey().endBefore(firstKey).limitToLast(10).once('value')).val()

        const keys = Object.keys(previousData)

        setFirstKey(keys[0])
        setLastKey(keys[keys.length - 1])
        setDefaults(oldDefaults => convertToArray(previousData))
    }

    const firstPageLoad = async () => {
        const firstPage = await (await database.ref('/system').orderByKey().limitToFirst(10).once('value')).val()
        const keys = Object.keys(firstPage)
        setFirstKey(keys[0])
        setLastKey(keys[keys.length - 1])
        setDefaults(oldDefaults => convertToArray(firstPage))
    }

    useEffect(() => {
        console.log('Called')
        firstPageLoad()
        document.querySelector('#nextButton').addEventListener('click', nextButtonListener)
        document.querySelector('#previousButton').addEventListener('click', previousButtonListener)

        return () => {
            document.querySelector('#nextButton').removeEventListener('click', nextButtonListener)
            document.querySelector('#previousButton').removeEventListener('click', previousButtonListener)
        }
    }, [])

    return (
        <div>
            <h2>Dashboard</h2>
            {defaults.map(product => {
                return (<Product key={product.id} product={{ id: product.id, amount: product.amount, name: product.productName }} />)
            }
            )}
            <button id="nextButton">Next</button>
            <button id="previousButton">Previous</button>
        </div>
    )
}

export default Dashboard

Product.js

import React, { useContext, useEffect } from 'react'
import { cartContext } from '../context/appContext'
import { addProduct, removeProduct } from '../actions/cart'

const Product = ({ product, isCart }) => {

    const { cartDispatch } = useContext(cartContext)

    return (
        <div>
            <h3>{product.name}</h3>
            <p>Amount: {product.amount}</p>
            {
                isCart ?
                    (<button onClick={() => { cartDispatch(removeProduct(product.id)) }}>Remove</button>) : (<button onClick={() => { cartDispatch(addProduct(product)) }}>Add to Cart</button>)
            }
        </div>
    )
}

export default Product

Now, everything renders perfectly but whenever I click on the button in Product , useEffect in Dashboard runs even though I have provided empty array as second arguement in useEffect method in MyComponent

D_Gamer
  • 168
  • 12
  • 3
    The `useEffect` hook callback ***can't*** be an `async` function. What is the "button in `SomeComponent`" doing when it's clicked? – Drew Reese Aug 23 '21 at 07:59
  • For what concerns your question, you mentioned that the undesired behavior occurs when you click on the button; it might be useful sharing the code of the event handler. On a side note: if your `useEffect()` hook sets some state, then `setSomeState` should be listed in the dependencies array. – secan Aug 23 '21 at 08:08
  • I feel that you provided an incomplete code – I am L Aug 23 '21 at 08:08
  • Yeah, My bad ! now I have uploaded the whole code . – D_Gamer Aug 23 '21 at 13:05
  • It looks like you might be misunderstanding a few base concepts about how React works. For example, you should not set event listeners in the useEffect() hook but simply assign the functions to the `onClick` prop of the button (and, more in general, if you have to access a DOM element you should use Refs rather than `document.querySelector()`). Another thing that does not make a lot of sense is `setDefaults(oldDefaults => convertToArray(firstPage))`: why are you using a callback function expecting an arguments that you do not use, to set the state? [...] – secan Aug 23 '21 at 13:26
  • [...] Also, in Products.js you seem to try accessing a context for whom it does not appear to be any provider – secan Aug 23 '21 at 13:28
  • @secan I am confused 'coz Those eventListeners are async functions and will they work in onClick call ?! I am using a callback function in setDefaults() 'coz I read an article about useEffect having the same problem and in there the author suggested to use function in setState methods instead of passing the new state directly you can find that article [here](https://daveceddia.com/useeffect-triggers-every-change/#:~:text=%20Fix%20useEffect%20re-running%20on%20every%20render%20,magical%20incantation%20sometimes.%20Mostly%2C%20it%E2%80%99s%20that...%20More%20) – D_Gamer Aug 23 '21 at 14:30
  • and about that context , the Whole Dashboard Component is part or App Router and Context has been shared in AppRouter so it is working just fine. – D_Gamer Aug 23 '21 at 14:36
  • Whether you do `document.querySelector('#nextButton').addEventListener('click', nextButtonListener)` or `` you are calling the same async function when the same event occurs; if it works in one case, it works in the other case too... but the first version is a direct access to the "real" DOM while the entire philosophy behind React is to work with a virtual DOM. [...] – secan Aug 23 '21 at 15:05
  • [...] For what concerns the way of setting the state, in the article you linked, the new state depends on the old one (`setList(oldList => oldList.map(...))` - `oldList` appears on both sides of the `=>`) but in your code the new state does not depend on the old one (in fact you have `setDefaults(oldDefaults => convertToArray(nextData))` - `oldDefaults` appears only as argument, on the left side of the `=>` but is never actually used in the function body). – secan Aug 23 '21 at 15:05
  • Yeah I got it and I have changed those eventListeners to onclick calls and also I have changed setState calls. But what about the solution ?! – D_Gamer Aug 23 '21 at 15:21

2 Answers2

1

First, you can't use async function in an effect, but you can call an async function instead. Secondly, somewhere you have some conditions that hide and show this component, otherwise, this effect should run on mount only.

Anarno
  • 1,470
  • 9
  • 18
0

You have not to use async with useEffect. Otherwise, I think you are going to get response from Firebase, you can use useCallback with async and await rather than useEffect.

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

const MyComponent = () => {
  const [someState, setSomeState] = useState([]);

  const getData = useCallback(async () => {
    try {
      const response = await fetch('/api/v1/get-data-firebase', { responseType: 'json' });
      setSomeState(response.data);
      // or axios.get()
    } catch (err) {
      console.error(err);
    }
  }, []);

  useEffect(() => {
    getData();
  }, [getData]);

  return (
    <div>
      {
        someState.map(item => (
          <SomeComponent key={item.id} />
        )
      }
   </div>
 )
};
hotcakedev
  • 2,194
  • 1
  • 25
  • 47
  • If `getData` is supposed to be called only once, rather than creating a separate function, you can use an async IIFE directly into `useEffect()`: `useEffect(() => { (async () => { /* same code of the getData() function */ })() }, [setSomeState])` – secan Aug 23 '21 at 08:36
  • @secan Not that way. Are you sure about your way? – hotcakedev Aug 23 '21 at 08:40
  • Yes, I am sure. If you use an IIFE, it is not the `useEffect()` callback to be asynchronous; it would be just like executing an async function declared somewhere else. You can refer to [this article](https://javascript.plainenglish.io/how-to-use-async-function-in-react-hook-useeffect-typescript-js-6204a788a435) for further details – secan Aug 23 '21 at 08:58
  • @secan I was asking because you have set a dependency `setSomeState` for `useEffect`. I think it will cause infinite calling `useEffect`. – hotcakedev Aug 23 '21 at 09:03
  • Well, useEffect *does* depend on `setSomeState` but, as that is the function setting `someState` and it is granted to never change, there will be no infinite loop. The infinite loop would occur if you added `someState` (the state value, not its updating function) to the dependencies array – secan Aug 23 '21 at 09:06
  • ... actually, depending on the settings of your linter, you might even get a warning if you do not include `setSomeState` to the dependencies array. – secan Aug 23 '21 at 09:09
  • @secan, `is granted to never change`. I can't agree with this. How to use a state as a fixed value to never change? Can you try again on your local side? – hotcakedev Aug 23 '21 at 09:12
  • `someState` is the state variable containing the *value* of the current state and it will change, `setSomeState` is the *function* setting the value of `someState`. The *function* `setSomeState` (not the state variable `someState`) is what should be listed in the dependencies array and that function is granted not to change. – secan Aug 23 '21 at 09:23
  • from the [React Hooks Reference](https://reactjs.org/docs/hooks-reference.html#usestate): "*React guarantees that `setState` function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the `useEffect` or `useCallback` dependency list.*" – secan Aug 23 '21 at 10:07
  • @hotcakedev `useState` updater functions are ***guaranteed*** to be stable references. ***BUT*** the linter only sees it as a function so will suggest including it in the dependency array. Because it's a stable reference it's safe to include in the dependency array, it won't trigger the effect callback. – Drew Reese Aug 23 '21 at 15:13