3

I have created new custom selectbox using React. There is pre-populated array which I am loading on component load (using useEffect). When user search for any non existing country there would be a option to add. So I used useState hooked. Code is as under :-

const [countries, setCountries] = useState([]);

Countries list :-

[
  {"value": 1, "label": "Singapore"},
  {"value": 2, "label": "Malaysia"},
  {"value": 3, "label": "Indonesia"},
  {"value": 4, "label": "Phillipines"},
  {"value": 5, "label": "Thailand"},
  {"value": 6, "label": "India"},
  {"value": 7, "label": "Australia"},
  {"value": 8, "label": "Pakistan"}
]


const handleHTTPRequest = () => {
    service.getData() 
    .then(res => {
      setCountries(res);
    })
    .catch((err) => console.error(err))
  }

  useEffect(() => {
    handleHTTPRequest()
  })

I am checking the searched country in the array and if not exists I simply add in array

const addCountry = (country) => {
    let isRecordExist = countries.filter(c => c.label === country).length > 0 ? true : false;
    const obj = {
      value: countries.length + 1,
      label: country
    }
    let updatedVal = [...countries, obj]
    
    setSelectedCountry(country)

    if (!isRecordExist) {
      **setCountries**(updatedVal) // updating array
    }
  }

The problem is its not updating although I can see data in updatedVal.

Entire code is here :-

https://github.com/ananddeepsingh/react-selectbox

Anand Deep Singh
  • 2,560
  • 3
  • 22
  • 28
  • I dont know why you are saying its not working, I have just tried it and it seems to be adding it properly in the list for me. – Talmacel Marian Silviu Jul 25 '20 at 19:09
  • have you tried using my git repo. If possible can you please fork my git repo and test it. Might be I am doing something wrong – Anand Deep Singh Jul 25 '20 at 19:17
  • I cloned you're repo, installed all the dependencies and I have run it with yarn start...When I search for a place it does not exist, I can add it and it appears in the list as required. – Talmacel Marian Silviu Jul 25 '20 at 19:21
  • In my repo. I did some thing wrong to make this works. So, if you go to line no 23 in App.js, I am directly updating array rather than using setCountries method. Please modify in your local and then run – Anand Deep Singh Jul 25 '20 at 19:25
  • The problem is not in App.js, it's in your select-box.js. Give me a minute to take a deeper look. – Dejan Janjušević Jul 25 '20 at 20:06

3 Answers3

8

The problem appears to be that you're passing to useState() the array (updatedVal) that has its reference unchanged, thus it appears to React that your data hasn't been modified and it bails out without updating your state.

Try drop that unnecessary variable and do directly setCountries([...countries, obj])

Another minor fix about your code I may suggest: you may use Array.prototype.every() to make sure that every existing item has different label. It has two advantages over .filter() - it will stop looping right upon hitting duplicate (if one exists) and won't proceed till the end of array (as .filter() does), thus won't slow down unnecessarily re-render and it returns boolean, so you won't actually need extra variable for that.

Following is a quick demo of that approach:

const { useState, useEffect } = React,
      { render } = ReactDOM,
      rootNode = document.getElementById('root')
      
const CountryList = () => {
  const [countries, setCountries] = useState([])
  
  useEffect(() => {
    fetch('https://run.mocky.io/v3/40a13c3b-436e-418c-85e3-d3884666ca05')
      .then(res => res.json())
      .then(data => setCountries(data))
  }, [])
  
  const addCountry = e => {
    e.preventDefault()
    const countryName = new FormData(e.target).get('label')
    if(countries.every(({label}) => label != countryName))
      setCountries([
        ...countries,
        {
          label: countryName,
          value: countries.length+1
        }
      ])
    e.target.reset()
  }
  
  return !!countries.length && (
    <div>
      <ul>
        {
          countries.map(({value, label}) => (
            <li key={value}>{label}</li>
          ))
        }
      </ul>
      <form onSubmit={addCountry}>
        <input name="label" />
        <input type="submit" value="Add country" />
      </form>
    </div>
  )
}

render (
  <CountryList />,
  rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Yevhen Horbunkov
  • 14,965
  • 3
  • 20
  • 42
2

I have found the root problem. In your Select Box component you have:

 const [defaultValueoptions, setdefaultValueoptions] = useState(options);

It should be:

 const [defaultValueoptions, setdefaultValueoptions] = useState([]);

  useEffect(() => {
    setdefaultValueoptions(options);
  }, [options]);
Talmacel Marian Silviu
  • 1,596
  • 2
  • 6
  • 14
  • hey, I am not sure exactly what the problem is with your code as I did not have time to check all code but when you put a key on an element, everytime that key changes the element re-renders...and it will use the new state...voila! the problem might be better solved other way but in a time of need you do with what you can. – Talmacel Marian Silviu Jul 25 '20 at 19:47
  • Although this hack works but its not updating the updated value with country object. – Anand Deep Singh Jul 25 '20 at 19:53
1

The problem is that you are not updating the countries list inside your SelectBox component. The countries list shouldn't be managed by the App component but by SelectBox component, as it's a stateful component. You are trying to get React to rerender your SelectBox component by changing the state of the App component, which won't work. Well it rerenders because you change a prop options, but it uses defaultValueoptions to display options, which you never update.

Add this to your SelectBox component:

useEffect(() => {
  setDefaultValueOptions(options);
}, [options]);

And change the line 9 to:

const [defaultValueoptions, setDefaultValueOptions] = useState(options);

EDIT I didn't see that Talmacel Marian Silviu had already edited his answer: https://stackoverflow.com/a/63092808/828023

Dejan Janjušević
  • 3,181
  • 4
  • 41
  • 67