ps1234
ps1234

Reputation: 161

React useEffect on function props

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

Answers (2)

resolritter
resolritter

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

Cully
Cully

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

Related Questions