0

I am learning react from helinski fsopen20. One of the exercises requires to click a button and show the weather of that button bound region.

The button has an onClick event that takes index of regions as the parameter to determine which region is selected (obviously).

<button onClick={() => onClickShow(i)}>{showBox.includes(i) ? myfunction(i): 'show'}</button>

OnClick function then renders details for the said region. The data required to be fetched is being done inside this function

const myfunction = useCallback((i)=>{ // the 'i' is passed
    
      axios.get('http://api.weatherstack.com/current'
      + `?access_key=${process.env.REACT_APP_API_KEY}`
      +`&query=${searchResult[i].name}`)
      .then(response=>{
        setWeather(response.data)
      })
      console.log(weather); //this re-renders to infinity
      
    return[
      <h2 key='name'>{searchResult[i].name}</h2>,
      <h2 key='sth'>Capital weather: {weather}</h2> // I will beautify this function later
    ]
   },[searchResult, weather])

   useEffect(()=>{

  },[myfunction])
   

I am able to achieve what I want but it costs a lot of re-rendering.

Initially using axios get() WITHOUT useEffect or useCallback resulted in infinite re-render BUT to my surprise,

I have tried useEffect and useCallBack() but nothing is stopping the re-rendering.

On a sidenote, I am using a different useEffect inside my App component which renders once just fine.

How do I properly use useEffect with eventhandler function such as onClick?

Below is the complete code:

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

const Helper=({searchResult})=>{

  const [showBox, setShowBox] = useState([])
  const [weather, setWeather] = useState([])


  const onClickShow = (index) => setShowBox(showBox.concat(index))

  
  const myfunction = useCallback((i)=>{
    
      axios.get('http://api.weatherstack.com/current'
      + `?access_key=${process.env.REACT_APP_API_KEY}`
      +`&query=${searchResult[i].name}`)
      .then(response=>{
        setWeather(response.data)
      })
      console.log(weather); /////// INFINTE RE-RENDER
      
    return[
      <h2 key='name'>{searchResult[i].name}</h2>,
      <p key='capital'>{searchResult[i].capital}</p>,
      <p key='popn'>{searchResult[i].population}</p>,
      <h3 key='langs'>Languages</h3>,
      <ul key='lang'>{searchResult[i].languages.map(lang => <li key={lang.iso639_1}>{lang.name}</li>)}</ul>,
      <img key='img' src={searchResult[i].flag} alt="flag" width="100" height="100" object-fit="fill"/>



    /*   searchResult.map(result=><h2 key={result.population}>{result.name}<br/></h2>),
    //   searchResult.map(result=><p key={result.population}> {result.capital} <br/> {result.population}</p>),
    //   <h3 key="id">Languages</h3>,
    //   searchResult.map(result=> <ul key={result.population}>{result.languages.map(lang => <li key={lang.iso639_1}>{lang.name}</li>)}</ul>),
    //   searchResult.map(result=><img src={result.flag} alt="flag" width="100" height="100" object-fit="fill" key={result.population}/>)
    */
    ]
   },[searchResult, weather])
   useEffect(()=>{

  },[myfunction])
   

  
  if(searchResult.length === 1){
      return(
        <div>
          {myfunction(0)}
        </div>
    )
  }
  else{
    return(
      <>
    {
        searchResult.length <= 10 ? 
        searchResult.map((result,i) => <h3 key={result.name}> {result.name} 
        <button onClick={() => onClickShow(i)}>{showBox.includes(i) ? myfunction(i): 'show'}</button></h3>)
        : searchResult
      }
        
          
      
      </>
    )
  }

  
}


const App =()=>{
  /// store all countries fetched
  const [countries, setCountries] = useState([])

  // store each searched country
  const [searchName, setSearchName] = useState([])
  
  //store the result country
  const [searchResult, setSearchResult] = useState([])



  useEffect(()=>{
    axios.get('https://restcountries.eu/rest/v2/all')
    .then(response=>{
      setCountries(response.data)
    })
   
  }, [])

  const handleSearch = (event) =>{
    setSearchName(event.target.value)
    if (searchName.length !== 0){

      var found = searchName ? countries.filter(country => country.name.toUpperCase().includes(searchName.toUpperCase())) : countries
      if(found.length > 10){
        setSearchResult("Too many matches, specify another filter")
      }
      else if(found.length === 1){
        setSearchResult(found)
      }
      else if(found.length === 0){
        setSearchResult(found)
      }
      else{
        setSearchResult(found)
      }
     
    }

  }


  

  return(
    <>

    <h1>find countries <input value={searchName} onChange={handleSearch} /></h1>
    <Helper searchResult={searchResult} />
    </>
  )
}

export default App;
  • simply remove `weather` from dependency list, that’s all. FYI, I posted another [answer here](https://stackoverflow.com/a/64114536/3617380) in which I suggested a `useFn` custom hook. Use that hook in place of useCallback can also solve your problem. – hackape Jan 24 '21 at 16:38

2 Answers2

0

This is happening because in the dependency array of usecallback, you are passing weather and inside the function, you are setting the state weather. So for the first time, it will be invoked and will set the state i.e weather is updated now which in turn will trigger the usecallback as it will be memoizng a function and whenever value updates of any one of the dependency array elements , it will be invoked.

This will continue likewise. To solve that, either you can remove the weather from the array or can give an empty array from usecallback.But in your case, api call depends on searchResult.So it should be present in the dependency array to trigger the invocation whenever value changes.

const myfunction = useCallback((i)=>{ // the 'i' is passed
    // axios call and return statement
   },[searchResult])  // <<<<<<<<<<<<<<<<<<<< weather has been removed

   useEffect(()=>{

  },[myfunction])
   

For more on usecallback

  1. link1
  2. link2
teddcp
  • 1,514
  • 2
  • 11
  • 25
  • That doesn't stop the re-rendering and doesn't help to append fetched data to 'weather' state. 'weather' is a dependency and if not passed to useCallBack, it wouldn't get updated – ribash sharma Jan 23 '21 at 16:49
0

The main issue is that you call myfunction as part of the render process.

const myfunction = useCallback((i) => {
  axios.get(
    'http://api.weatherstack.com/current'
      + `?access_key=${process.env.REACT_APP_API_KEY}`
      + `&query=${searchResult[i].name}`
  )
  .then(response=> setWeather(response.data))

This causes axios to fire a request, which sets a new state if it gets a success response. Setting a state causes a re-render. This re-render again calls myfunction as part of it's render process, which sets a new state if successful. This causes a new re-render. etc.

You probably don't want to call the API for every render you do. Normally you would move web-request into an event handler or useEffect hook, at least something that is not triggered during rendering.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • Thank you, now I understand the concept. I am in dilemma how would I be able to call API once outside of "myfunction(i)" as the API fetch needs an 'i' passed into from 'myfunction()' – ribash sharma Jan 23 '21 at 19:22
  • @ribashsharma You can make the API call as part of your `onClick` handler. You can store the data inside a state (like you are already doing `setWeather(response.data)`). Then use this state during rendering. This way the API call only happens due to a user interaction and not by just rendering. You also have access to `i` inside the when setting the `onClick` event, since you are iterating `searchResult`. – 3limin4t0r Jan 23 '21 at 22:08
  • I am sorry but I couldn't get it to render only once. Could you please throw me a small snippet of code? – ribash sharma Jan 24 '21 at 01:39