1

I'm trying to update some content on the screen upon a change in the data. I'm using React function component with hooks. When the data is updated in the parent element, I send it via props to PlayerControls:

export const PlayerControls = (props) => {
....

  const [heatmaps, setHeatMaps] = React.useState("");
  const [data, setData] = React.useState(null);

  useEffect(fetchData, [props.videoData]);
  useEffect(updateHeatmaps, [data]);

  function fetchData () {
     d3.csv(require(`./data/${props.videoData}`)).then((d) => setData(d));
  }

  function updateHeatmaps(){
        if (data && data.length) {
            const temp = data.map((item, index) => {
                return (
                    <Heatmap size={[400, 50]} data={item} key={index}/>
                )
            });
        setHeatMaps(temp);
        }
   }

   return (
       <div>
         {heatmaps}
       </div>
   );

}

The problem is, although this works properly for the initial data, it does not update heatmaps on the screen. Now, I'm familiar with the fact that setState hook is asynchronous and there are solutions (such as using the useEffect hook to update the state and add the state as a dependency), e.g., this StackOverflow question. The problem is I'm updating the state withing another function and I cannot use useEffect there or in the return function. I would really appreciate any solutions, this problem has been dragging me along for a while now, makes me wonder if these hooks and function components are a solution for all the use cases or not.

Mahsan Nourani
  • 294
  • 2
  • 17

2 Answers2

1

Instead of rendering the heatmaps string, return the mapped data if it exists when rendering:

export const PlayerControls = (props) => {
    const [data, setData] = React.useState(null);
    useEffect(fetchData, [props.videoData]);

    function fetchData() {
        d3.csv(require(`./data/${props.videoData}`)).then((setData);
    }

    return (
        <div>
            {data?.map((item, index) => (
                <Heatmap size={[400, 50]} data={item} key={index} />
            ))}
        </div>
    );
}

Example snippet:

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const getData = () => Promise.resolve(['foo', 'bar']);
const PlayerControls = (props) => {
    const [data, setData] = React.useState(null);
    React.useEffect(fetchData, [props.videoData]);

    function fetchData() {
        getData().then(setData);
    }

    return (
        <div>
            {data && data.map((item, index) => (
                <Heatmap size={[400, 50]} data={item} key={index} />
            ))}
        </div>
    );
}
const Heatmap = ({ data }) => <div>{data}</div>;
ReactDOM.render(<PlayerControls />, document.querySelector('.react'));
</script>
<div class="react"></div>
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • I was really excited about your answer, but it doesn't work. My guess is that your solution doesn't detect that the data has changed? – Mahsan Nourani Sep 09 '20 at 18:18
  • Oh, the `updateHeatmaps` isn't used anymore, so it can be completely removed. Was probably throwing a ReferenceError before – CertainPerformance Sep 09 '20 at 18:21
  • Yeah, I get you, but it still doesn't work and my guess is that somehow, nothing triggers the change in the data. (to be more specific, when I say it does not work, I mean it doesn't show the change in your solution, similar to my solution) -- although, your solution still works for the initial data. :( – Mahsan Nourani Sep 09 '20 at 18:33
  • Any console errors? Are you sure the `.then` is running? (As always, it's good to add a `.catch` to Promises to check if they reject) Is `setData` called with an array? – CertainPerformance Sep 09 '20 at 18:59
  • Yes, I print "d" inside `then` function and it shows the updated data. But of course, it does not update the "data" through `setData` because useState hooks are async. That's why I guess the problem still exists with your code because react does get notified when `data` is being updated. – Mahsan Nourani Sep 09 '20 at 20:16
  • 1
    I made an example snippet with a dummy Promise. It looks to work as desired - press "Run code snippet" to see `foo` and `bar` being rendered. If it doesn't work on your end, it sounds like the problem is outside of the code posted in the question – CertainPerformance Sep 10 '20 at 01:11
  • Thank you for your response! Yes, apparently, there was a silly mistake somewhere and I'm going to post it as the answer. But your code is a good substitute and I really liked it in general! I'm upvoting your answer :) – Mahsan Nourani Sep 10 '20 at 02:11
0

So, as @CertainPerformance mentioned in the comments above, the problem was not with the useState hooks, but it was from within the updateHeatmap function. Apparently, when using .map function, your keys should be globally unique. I used index as the key, and since this index did not change after the update, React didn't detect a change and did not update the heatmaps state.

Changing

const temp = data.map((item, index) => {
     return (
             <Heatmap size={[400, 50]} data={item} key={index}/>
            )
     });

to

const temp = data.map((item, index) => {
     let key = props.videoData + '.' + index;
     return (
             <Heatmap size={[400, 50]} data={item} key={key}/>
            )
     });

fixed my problem. I hope this helps other people facing similar issues!

Mahsan Nourani
  • 294
  • 2
  • 17