Conversation
In the results view, `setState` was used to set some state, and then `loadResults` was called to set some other state. However, `setState` is asynchronous, so the `this.state` in `loadResults` was not the state that was set before the call. This commit fixes it by combining the two `setState` calls into one. The logic was hard to follow, so I'm not sure if this is the correct fix. The `shouldKeepOldResultsWhileRendering` state seems to be unnecessary now since everything is being done in `setState` call, but I'm not sure about that.
|
Thanks for the review @charisk! I've had to make some other changes to make it fully work with React 18. The results view wasn't working due to how React is now batching state updates, so there were quite some changes in how the results view now receives some messages. @aeisenberg Would you be able to review those changes as well? It's mostly about 36eb2cd. I don't believe there are any major behaviour changes, but we're not using |
|
|
||
| private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void { | ||
| this.setState((prevState) => { | ||
| const stateWithDisplayedResults = ( |
There was a problem hiding this comment.
Could we write tests for the behaviour before we made this change and then see if the same behaviour happens afterwards? That would mean we wouldn't need to guess if we've completely got it right.
There was a problem hiding this comment.
I think writing tests for this is really hard and the previous behaviour will not be observable in tests. Adding tests would probably take at least a day, and since we'll be rewriting these views soon anyway, I don't really see a lot of added value in it.
There was a problem hiding this comment.
Is it hard because we would have to check visual components or because it's a timing issue?
There was a problem hiding this comment.
It's a timing issue, there's no real way to check the state in between the two setState calls, so we'd never get into the state that we'd want to test. We'd want to check what the view looks like in between these two calls but they are called sequentially without any waiting, so there's no way to actually test that state.
There was a problem hiding this comment.
I'm gonna defer to @aeisenberg on this one. I would ask if we can get rid of shouldKeepOldResultsWhileRendering entirely if we're combining calls.
There was a problem hiding this comment.
Looks like Andrew is on holiday so you might have to wait a bit with this one.
There was a problem hiding this comment.
Just wanted to say that I wasn't expecting us to do a complete re-write of this view soon as the changes we'd like to make are mostly cosmetic. But I can be convinced otherwise. Also I don't mean that to say that we have to spend a day writing tests - the component style does look a bit old so likely not worth the investment.
There was a problem hiding this comment.
This method is overly complex and a simplification would be really nice. To be safe when we are doing a refactoring like this, it would be nice to add some pure regression unit tests to ensure that all the different cases are handled identically before and after.
Although nice, it's not necessary to spend the time to create more complex tests that check the view between the two setState calls. All that I think is necessary is that we ensure the method behaves the same before and after the refactoring. It's a complicated method, but at least the complexity is contained.
There was a problem hiding this comment.
I've created a really simple regression test in #2255. This does not test the loading state, but checks that we can at least render a SARIF file.
|
Do we get any extra warnings now that we've upgraded to React 18? I'd be surprised if we have a clean upgrade. |
This removes the comment about the `viewLoaded` callback since it refers to upgrade instructions.
We don't have any extra warnings since we're actually not using that many React features. What kind of warnings would you have expected to see? |
|
I was expecting to see warnings when we run |
Thanks for the poke! The changes look okay to me, but as you say the logic is quite hard to follow. I've also tested them a bit manually and things seem ok to me. |
|
Thanks for testing @charisk! I wasn't sure how to go about checking this. If we know what to expect in the behaviour change, I don't want to hold this up. |
Sorry I wasn't clear - I didn't necessarily test the specific use case. I just poked the view a bit. |
| isExpectingResultsUpdate: prevState.isExpectingResultsUpdate, | ||
| nextResultsInfo: resultsInfo, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Are you missing a case here? I don't see line 191 from the original handled.
return stateWithDisplayedResults(prevState.displayedResults);
There was a problem hiding this comment.
This is some gnarly updating code. Just eyeballing things, is this a better simplification:
this.setState((prevState) => {
let displayedResults: ResultsState | undefined = undefined;
if (!prevState.isExpectingResultsUpdate && resultsInfo === null) {
// No results to display
displayedResults = {
resultsInfo: null,
results: null,
errorMessage: "No results to display",
};
} else if (!resultsInfo?.shouldKeepOldResultsWhileRendering) {
// Display loading message
displayedResults = {
resultsInfo: null,
results: null,
errorMessage: "Loading results…",
};
} else {
displayedResults = prevState.displayedResults;
}
return {
displayedResults,
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
nextResultsInfo: resultsInfo,
};
});Unit tests would be great for making sure the semantics of updateStateWithNewResultsInfo haven't changed.
There was a problem hiding this comment.
In this case, the missing case is correct. In the previous code, after the setState returned with stateWithDisplayedResults(prevState.displayedResults), this.loadResults() would be called immediately. The only reason for storing the nextResultsInfo in the state is so that loadResults can use it. However, with concurrent updates the state seen in loadResults would not yet be the state set in this setState call. Therefore, I've combined this setState and the loadResults method. Therefore, this case won't happen anymore.
Thanks for the suggestion for the simplification. Since we don't need the third case, I couldn't use it as-is. However, I have split the flows differently so there's only 1 level of nesting rather than 2.
|
|
||
| private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void { | ||
| this.setState((prevState) => { | ||
| const stateWithDisplayedResults = ( |
There was a problem hiding this comment.
This method is overly complex and a simplification would be really nice. To be safe when we are doing a refactoring like this, it would be nice to add some pure regression unit tests to ensure that all the different cases are handled identically before and after.
Although nice, it's not necessary to spend the time to create more complex tests that check the view between the two setState calls. All that I think is necessary is that we ensure the method behaves the same before and after the refactoring. It's a complicated method, but at least the complexity is contained.
aeisenberg
left a comment
There was a problem hiding this comment.
This looks good now, but can you rebase on #2255 so we can be sure that the behaviour remains consistent?
This upgrades the webviews to use React 18. There should be no user-facing changes.
Closes #1878
Checklist
ready-for-doc-reviewlabel there.