9

I'm making a custom dropdown, that allows for pushing new items in the dropdown. For some reason the useEffect is not being triggered on state change, but it being triggered on initial render. I'm pretty sure I'm missing something small, but can't see it. The new item should be pushed when a user clicks on the button tied to the 'addNewOptionToTree' method. The categoryList should then show the new item in the dropdown. The console log gets triggered and the new arr is present... Any ideas?

above return:

    const [newOption, setNewOption] = useState('')

    const [categoryList, setCategoryList] = useState(["Calendars", "Meetings", "Apostrophes work!"])

    useEffect(() => {
        console.log("categoryList::::::::::::::::", categoryList)
      }, [categoryList]);
    
    
      function addNewOptionToTree() {
        console.log('category:', categoryList);
        console.log('newOption:', newOption);
        const categoryListArr = categoryList
        categoryListArr.push(newOption)
        setCategoryList(categoryListArr)
        console.log("category:", categoryListArr);
    
      }

in return block:

<div className='dropDownList'>
          <div className='listItem' key={'add-item-key'}>
            <Input
              type='text'
              label=''
              value={newOption}
              placeholder='Add New Category'
              onChange={val => setNewOption(val)}
            />
          <div className='icon-add-contain dropdown-add-btn' onClick={addNewOptionToTree}></div>
          </div>
          {
            categoryList.length > 0 &&
              categoryList.map((category, index) => (
                <div className='listItem' onClick={onOptionClicked(category)} key={'level1-'+index}>
                  {category}
                </div>
              ))
          }
        </div>
benishky
  • 901
  • 1
  • 11
  • 23

3 Answers3

16

In your case it's not being changed, because objects and arrays are compared by reference, not by value, in JS.

For example

let foo = {bar: 1}
let faz = foo
let notFoo = {bar: 1}
foo === faz // true
foo === notFoo // false

That being said, in here:

 const categoryListArr = categoryList // you are passing categoryList by reference
 categoryListArr.push(newOption)
 setCategoryList(categoryListArr)

you are mutating your state directly, which is not good usually. For this to work properly you need to create your categoryListArr array in an immutable way

 const categoryListArr = [...categoryList] // this is a new array, which contains the same items from the state
 categoryListArr.push(newOption)
 setCategoryList(categoryListArr)

or like this

setCategoryList(prev => [...prev, newOption])

Now your useEffect will be triggered.

k.s.
  • 2,964
  • 1
  • 25
  • 27
  • 1
    you can actually completely remove the `categoryListArr`, and just do `setCategoryList([...categoryList, newOption])` -> all the previous values, append newOption. although, I believe the correct way would be to `setCategoryList(prevList => [...prevList, newOption])` – tachko Jul 16 '20 at 21:23
2

The problem is that you are using array to compare and trigger the useEffect so it triggers on initial render as length of array changes but on subsequent changes if length is same and only any element is changed this wont trigger useEffect

You can use JSON.stringify

useEffect(() => {
        console.log("categoryList::::::::::::::::", categoryList)
      }, [JSON.stringify(categoryList)]);
1

Just change

const categoryListArr = categoryList
categoryListArr.push(newOption)
setCategoryList(categoryListArr)

in

setCategoryList([...categoryList, newOption]);

this will change the Array reference and let the effect to trigger.

Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55