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.
- Select file1. 1.1 file 1 data displayed on graph
- Select file2. 2.1 file 2 data displayed on graph
- Delete file 2 3.1 Only file 1 data not displayed in graph
- 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.
- 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:
If I then delete these lines, I expect to see no graph data, and the state appears to reflect this:
But! If I then click on a third file...
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.