41

I have two sibling components that share state via context in react. The shared state between the components is an array.

If I update arr state in one component, I want the other component to listen for that update and do something accordingly. When I use useEffect in the second component, I listen for changes in the arr state variable.

For example:

// App Component ------- const App = props => { const { arr, setArr } = useContext(GlobalContext) const handleChange = () => { const newArr = arr [10, 20, 30, 40].map(v => { newArr.push(v) setArr(newArr) }) return (...) } // App2 (Sibling) Component const App2 = props => { const { arr, setArr } = useContext(GlobalContext) const [localArr, setLocalArr] = useState(0) useEffect( () => { updateLocalState() }, // fire if "arr" gets updated [arr] ) const updateLocalState = () => { setLocalArr(localArr + 1) } return (...) } 

The useEffect hook is only fired on the initial render, though the state of arr updates.

I know that declaring a new variable const newArr = arr to my state variable is a reference, so newArr.push(v) is technically a state mutation. However, the state still updates, no warning is thrown, and useEffect does nothing.

Why does useEffect not get called though the state gets updated? Is it because of the state mutation?

Second Question: Why is there no warning or error thrown regarding a state mutation? State mutations are dangerous - If it happens, I'd expect some sort of warning.

Live demo here:

Edit 7wzlo8y4m1

6
  • 6
    You never create a new array. const newArr = arr references the same array with another variable. Try e.g. const newArr = [...arr] instead. Commented Feb 10, 2019 at 20:58
  • 1
    It is a reference to the same array, but that doesn't explain why useEffect doesn't fire though the state still updates. Commented Feb 10, 2019 at 20:59
  • 2
    The array you pass as second argument to useEffect only checks if the elements in the array are === to the elements in it in the previous render. const newArr = arr; will lead to newArr === arr, which is not what you want. Commented Feb 10, 2019 at 21:01
  • 5
    what @Tholle says, just changing setArr([...newArr]) fires useEffect correctly Commented Feb 10, 2019 at 21:02
  • @ArtyomNeustroev your comment solved my problem! Thanks a TON. Commented Dec 23, 2022 at 18:04

2 Answers 2

56

The array you pass as second argument to useEffect only checks if the elements in the array are === to the elements in it in the previous render. const newArr = arr; will lead to newArr === arr since it doesn't create a new array, which is not what you want.

Create a new array with all the elements in arr and it will work as expected.

const App = props => { const { arr, setArr } = useContext(GlobalContext) const handleChange = () => { const newArr = [...arr] [10, 20, 30, 40].forEach(v => { newArr.push(v) }) setArr(newArr) } return <>{/* ... */}</> } 
Sign up to request clarification or add additional context in comments.

5 Comments

I think it might be worth showcasing a better practice for this block of code - something as simple as setArr([...arr, 10, 20, 30, 40])
I hadn't considered the diffing algorithm in useEffect being a strict comparison and it being the same object for that comparison. That makes sense.
I wonder why no error or warning gets thrown about the state mutation?
@lsimonetti There's no straight-forward way for React to check for mutation let alone warn when you do it, sadly. It's just a limitation of the language.
This helps but i am still confused.
24

When you want to update array using useState hook. Make sure to spread the array into new array and update the new array so that your useEffect listening for this state will be called.

UseEffect will not call in the below code snippet as you are directly updating array.

const [skills, selectedSkills] = useState([]) const onSelect = (selectedList) => { selectedSkills(selectedList) } useEffect(() => { MyLogger('useEffect called') }, [skills]) 

UseEffect will call in the below code snippet as we are keeping new reference to the array.

const [skills, selectedSkills] = useState([]) const onSelect = (selectedList) => { const tempSelectedList = [...selectedList] selectedSkills(tempSelectedList) } useEffect(() => { MyLogger('useEffect called') }, [skills]) 

2 Comments

misleading answer. you do not know if selectedList in the first example is mutated or is a new array
my problem was that i forgot to put my variable in the array at the bottom, where you have [skills]

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.