Reputation: 161
Just started using React function hooks. I was looking into a case where you have a parent function which passes an array and a child function which relies on the array contents.
App.js (parent) ->
import React, {useEffect} from 'react';
import ChartWrapper from './StaticChart/ChartWrapper';
import './App.css';
function App() {
let data = [];
function newVal() {
let obj = null;
let val = Math.floor(Math.random() * 10);
let timeNow = Date.now();
obj = {'time': timeNow, 'value': val};
data.push(obj);
// console.log(obj);
}
useEffect(
() => {
setInterval(() => {newVal();}, 500);
}, []
);
return (
<div className="App">
<ChartWrapper data={data} />
</div>
);
}
export default App;
ChartWrapper.js (child) ->
import React, {useRef, useEffect} from 'react';
export default function ChartWrapper(props) {
const svgRef = useRef();
const navRef = useRef();
useEffect(
() => {
console.log('function called');
}, [props.data]
);
return (
<div>
<svg ref = {svgRef} width = {700} height = {400}></svg>
<svg ref = {navRef} width = {700} height = {75}></svg>
</div>
);
}
So every time App.js adds a new object in the array, ChartWrapper gets called. Currently, in the ChartWrapper function, I have useEffect which should listen to changes in props.data array and get executed, but it is not working. The only time the function gets called is initially when the component renders.
What am I doing wrong?
Upvotes: 1
Views: 513
Reputation: 331
Objects in hooks' dependencies are compared by reference. Since arrays are objects as well, when you say data.push
the reference to the array does not change and, therefore, the hook is not triggered.
Primitive values, on the other hand, are compared by value. Since data.length
is a primitive type (number
), for your purpose, putting the dependency on data.length
instead would do the trick.
However, if you were not modifying the array's length, only the values inside of it, the easiest way to trigger a reference difference (as explained in the first paragraph) would be to wrap up this array in a setState
hook.
Here's a working example: https://codesandbox.io/s/xenodochial-murdock-qlto0. Changing the dependency in the Child component from [data.length]
to [data]
has no difference.
Upvotes: 1
Reputation: 6955
Ok, I ran your code and figured out what the problem is. data
is changing, but it isn't triggering a re-render of ChartWrapper
. So your useEffect
in ChartWrapper
is only run once (on the initial render). In order to trigger a re-render, you'll need to create and update data
using useState
:
let [data, setData] = useState([])
function newVal() {
let obj = null;
let val = Math.floor(Math.random() * 10);
let timeNow = Date.now();
obj = {'time': timeNow, 'value': val};
const newData = [...data]
newData.push(obj)
setData(newData)
}
However, that isn't quite enough. Since your setInterval
function is defined once, the value of data
in its call context will always be the initial value of data
(i.e. and empty array). You can see this by doing console.log(data)
in your newVal
function. To fix this, you could add data
as a dependency to that useEffect
as well:
useEffect(() => {
setInterval(newVal, 1000);
}, [data]);
But then you'll create a new setInterval
every time data changes; you'll end up with many of them running at once. You could clean up the setInterval by returning a function from useEffect
that will clearInterval
it:
useEffect(() => {
const interval = setInterval(newVal, 1000);
return () => clearInterval(interval)
}, [data]);
But at that point, you might as well use setTimeout
instead:
useEffect(() => {
const timeout = setTimeout(newVal, 1000);
return () => clearTimeout(timeout)
}, [data]);
This will cause a new setTimeout
to be created every time data
is changed, so it will run just like your setInterval
.
Anyway, your code would look something like this:
const ChartWrapper = (props) => {
useEffect(
() => {
console.log('function called');
}, [props.data]
);
return <p>asdf</p>
}
const App = () => {
let [data, setData] = useState([])
function newVal() {
let obj = null;
let val = Math.floor(Math.random() * 10);
let timeNow = Date.now();
obj = {'time': timeNow, 'value': val};
const newData = [...data]
newData.push(obj)
setData(newData)
}
useEffect(() => {
const timeout = setTimeout(newVal, 1000);
return () => clearTimeout(timeout)
}, [data]);
return (
<ChartWrapper data={data} />
)
}
And a running example: https://jsfiddle.net/kjypx0m7/3/
Something to note is that this runs fine with the ChartWrapper's useEffect
dependency just being [props.data]
. This is because when setData
is called, it is always passed a new array. So props.data
is actually different when ChartWrapper re-renders.
Upvotes: 2