setState wasn't working - annoying bug postmortem!
I encountered a very annoying bug in a piece of basic React code yesterday, and it took me and another experienced developer far too long to spot the problem.
Here is the screenshot I posted to Slack after spending no small amount of time already trying to figure it out:
We have an annoying issue in our product where an informative screen element truncates long messages, so we wanted a simple solution of rendering a modal with the full message when the truncated version was clicked. VERY EASY to do with React, right?
So the code above purports to do that. Click on the truncated message element: modal pops up; click on the 'Ok' button on the modal: modal is dismissed.
Here is simple reproduction of the bug on CodePen:
See the Pen Bug Demo 7-5-19 by Jackson Bates (@Malgalin) on CodePen.
The only real modifications I've made to the above app are the conditionally rendered div in place of the modal, and the click counter that proves that the close handler method works and can set the state. You can click the babel
tab above to see the code, and the result
tab to toggle the app view away.
Diagnosing the problem #
If you Google any version of 'setState is not working' you will encounter a gazillion StackOverflow answers and blog posts that will tell you that the issue is that setState
is asynchronous.
Great, who cares? That's not the problem here, right? Because we are calling an identically formatted setState function to dismiss the modal as the one that made it appear.
We added console.logging within the setState callback to ensure that the setState code was being reached. It was. Then we added a click counter like the one in the reproduction to prove that we could update state with that exact method. We could.
Eventually we determined that there must be a race condition somewhere, somehow. Even though we were certain nothing else was trying to set the state, something must have been.
Look again at the code.
- The
<Container>
has the initial onClick listener.- Clicking the
<Container>
triggers setState.
- Clicking the
- The
<InformationDialog>
has its own onClick listener.- Clicking the
<InformationDialog>
triggers setState... - but also, something else is triggering setState again with a competing value.
- Clicking the
Have you spotted it already?
See the Pen Bug-Fix Demo 7-5-19 by Jackson Bates (@Malgalin) on CodePen.
The problem is that setState is asynchronous #
So, setState is asynchronous...AND the modal was nested in a greedy container.
When clicking the modal, you are also clicking the container again, introducing the race condition.
The solution is to separate the onClick listeners appropriately so you are only clicking one thing at a time.
Fun times were had by all. What did we learn?
Not sure.
Be better, I guess.