Cloud Stack Ninja

I have a scenario where I have a set of data where a filename maps to a set of graph line data. On a checkbox change, it will either delete from or add to the map (not a JS map - just a JS object). The add operation works fine, at first. The delete operation also appears to work, and after deleting a filename from the map the React state appears to have updated correctly. However, when adding another item react seems to "resurrect" a very old state that represents the collection of all files ever added to the map. This behavior is caused by a specific setState() call, which I highlight below. Been going round in circles debugging this and have hit a dead end. The reason state is cloned in the console.log() call is that I found out that the Chrome console.log calls are asynchronous. I was being a bit more choosey about what I was deep cloning but to remove any uncertainty I just deep cloned everything.

What exactly break is.

  1. Select file1. 1.1 file 1 data displayed on graph
  2. Select file2. 2.1 file 2 data displayed on graph
  3. Delete file 2 3.1 Only file 1 data not displayed in graph
  4. Delete file 1 4.1 Nothing displayed on graph. this.state appears to reflect this both in the console.log output and the react development tools inspection of the element state.
  5. Select any file, but lets say file 3 5.1 Graph displays data for file1, file2, and file3. 5.2 This is wrong - in 4.1 saw that the state showed no graph data was mapped. The error is introduced by a setState() call that only updates one, unrelated flag, that is used to show a modal dialog.

If I remove the call this.setState({fetching_data: true},... from getFIleData() then everything works. For some reason if that call is there, it receives as older state.

If anyone can shed light on this, I would be most grateful, as I have run out of ideas ;)

class ResultsList extends React.Component
{
    state = {
        fetching_data: false,

        // Data on all available files, for which server can provide data
        //
        // [ {
        //    date: (5) [mm, dd, hh, mm, ss]
        //    pm: "pmX"
        //    fullname: "mm_dd_hh_mm_ss_pmX_TAG"
        //    tag: "TAG"
        //    serno: "1234"
        //   },
        //   ...
        // ]
        data : [],

        selected_col: "",

        // Contents of data files that have been requested from server. If item is in this
        // dictionary then is is "selected", i.e. displayed on the graph. When "deselected"
        // should be removed from this dictionary.
        //
        // { filename1 : {
        //       data: {dataset1: Array(45), dataset2: Array(45), xvalues: Array(45)}
        //       path: "blah/blah"
        //       status: 200
        //       status_str: "ok"
        //       >>>> These bits are augmented, the above is from server
        //       FAM_colour: string,
        //       HEX_color: string,
        //       <<<<
        //    },
        //    filename2 : {
        //       ...
        //    }
        // }
        file_data: {},
        file_data_size: 0,

        graph: null,
    };

    createGraphDataSetsFromFileData = (srcFileData) => {
        const newGraphDatasets = [];
        let idx_prop = 0;
        for (var prop in srcFileData) {
            if (Object.prototype.hasOwnProperty.call(srcFileData, prop)) {
                newGraphDatasets.push(
                    {
                        label: 'dataset1_' + prop,
                        fill: false,
                        lineTension: 0.5,
                        backgroundColor: 'rgba(75,192,192,1)',
                        borderColor: srcFileData[prop].FAM_colour,
                        borderWidth: 2,
                        data: srcFileData[prop].data['dataset1'],
                    }
                );
                newGraphDatasets.push(
                    {
                        label: 'dataset2_' + prop,
                        fill: false,
                        lineTension: 0.5,
                        backgroundColor: 'rgba(75,192,192,1)',
                        borderColor: srcFileData[prop].HEX_colour,
                        borderWidth: 2,
                        data: srcFileData[prop].data['dataset2'],
                    }
                );
                idx_prop = idx_prop + 1;
            }
        }
        return newGraphDatasets;
    };

    getFIleData = (filename) => {
        console.log("GETTING OPTICS");
        console.log(cloneDeep(this.state));

//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// If I remove this setState call then everything works
//
        // Display the "fetching data" modal dialog
        this.setState({fetching_data: true},
            () => { 
                console.log("££££££ DIALOG SHOW COMPLETED ££££££"); console.log(cloneDeep(this.state));
            }
        );
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        fetch(`http://${this.SERVER_IP_PORT}/api/v1/get_result/${filename}`)
          .then(response => response.json())
          .then(json => {
            console.log("JSON REPLY RECEIVED")
            if (json.status !== 200) {
              alert("Failed to fetch results list")
            }
            else {
                const EXPECTED_NO_CYCLES = 45 // Oh so terribly hacky!!!
                if (json.data['dataset1'].length === EXPECTED_NO_CYCLES) {
                    this.setState( (prevState) => {
                        console.log("JSON SETSTATE PREV STATE AS"); console.log(prevState);
                        
                        // Clone the file_data map and add the new data to it
                        let newFileData = cloneDeep(prevState.file_data);
                        newFileData[filename] = cloneDeep(json); // TODO - FIXME - BAD this is a shallow copy
                        newFileData[filename].FAM_colour = COLD_COLOURS[prevState.file_data_size % COLD_COLOURS.length];
                        newFileData[filename].HEX_colour = WARM_COLOURS[prevState.file_data_size % WARM_COLOURS.length];

                        // Create new graph data from the file_data map
                        let newGraph = null;
                        if (newGraph === null) { 
                            newGraph = {
                                labels : cloneDeep(json.data['xvalues']),
                                datasets: this.createGraphDataSetsFromFileData(newFileData)
                            }
                        }
                        else {
                            newGraph = cloneDeep(prevState.graph)
                            newGraph.labels = cloneDeep(json.data['xvalues']);
                            newGraph.datasets = this.createGraphDataSetsFromFileData(newFileData)
                        }

                        const retval = {
                            file_data: newFileData,
                            file_data_size: prevState.file_data_size + 1,
                            graph : newGraph
                        };
                        console.log("------- returning:"); console.log(retval);
                        return retval;
                    }, () => {console.log("££££££ OPTICS STAT EUPDATE APPLIED ££££££"); console.log(cloneDeep(this.state)); });
                }
                else {
                    alert("Assay test run contains imcomplete data set");
                }
            }
          })
          .catch( error => {
            alert("Failed to fetch results list: " + error);
          })
          .finally( () => {
            console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
            this.setState({fetching_data:false},
                () => {console.log("££££££ OPTICS STAT FINALLY NOT FETCH APPLIED ££££££"); console.log(cloneDeep(this.state)); });
          });
    };

    //
    // THIS FUNCTION IS THE onChange FOR A CHECKBOX and is passed the filename of the item clicked
    handleRowSelectionChange = (fullname) => {
        if (this.state.file_data.hasOwnProperty(fullname)) { 
            console.log("CHECKBOX NOT TICKED")           
            this.setState( 
                (prevState) => {
                    // Delete the file from the map
                    let newFileData = cloneDeep(prevState.file_data);
                    delete newFileData[fullname];
                    
                    let rv = {
                        file_data: newFileData,
                        file_data_size: prevState.file_data_size - 1,
                        graph : {
                            datasets: this.createGraphDataSetsFromFileData(newFileData),
                            labels: cloneDeep(prevState.graph.labels)
                        }
                    }
                    console.log("______"); console.log(rv);
                    return rv;
                },
                () => {
                    console.log("======== DELETE UPDATE APPLIED =======");
                    console.log(cloneDeep(this.state));
                }
            );
        }
        else {
            console.log("CHECKBOX IS TICKED");
            this.getFIleData(fullname);
        }
    }

If I select two files, I expect 4 datasets in the graph and the state reflects this: enter image description here

If I then delete these lines, I expect to see no graph data, and the state appears to reflect this: enter image description here

But! If I then click on a third file... enter image description here

The old state is introduced, specifically by

this.setState({fetching_data: true},
            () => { 
                console.log("££££££ DIALOG SHOW COMPLETED ££££££"); console.log(cloneDeep(this.state));
            }
        );

in the getFileData function. If this is removed, then no stale state is introduced.



Read more here: https://stackoverflow.com/questions/64416460/react-provides-stale-state-in-setstate

Content Attribution

This content was originally published by Jimbo at Recent Questions - Stack Overflow, and is syndicated here via their RSS feed. You can read the original post over there.

%d bloggers like this: