componentWillUnmount is lazy?
See original GitHub issueI’m turning here because I ran into this issue and I have real difficulty explaining it.
I must admit I don’t know what I am doing exactly because I am porting someone else’s work and tests, but it was supposed to be an easy port and is turning out… not so easy… I am not using preact-compat
because I feel a true port should not need it.
I pushed my work so far so you can see what I’m doing: https://github.com/Download/preact-helmet/tree/port-to-preact
The problem:
I have a test failing in preact-helmet and I can’t explain it. It looks like componentWillUnmount
is called later than expected by the test. See the Travis run.
In the test I am forcing an unmount by rendering an EmptyComponent
. This is based on remarks from the issue tracker here and from preact-compat.
In each test, I render using this code:
child = render(
<Jsx>Bla</Jsx>
,
container, child
);
Here, container is a div nested in the document body, that is never replaced. Child is initially undefined
and then the result of each render
call.
In an after
hooks that runs after each test, I call unmountComponentAtNode
, which I have tweaked slightly from the one found in preact-compat:
function unmountComponentAtNode(container, child) {
return render(<EmptyComponent />, container, child);
function EmptyComponent() { return null; }
}
Analysis so far:
I debugged this the ugly way: by placing console.info
statements in the code.
Firs I placed a log statement in unmountComponentAtNode
. That confirms that this function is indeed called after each test.
Next I placed two log statements in preact-side-effect
, that this module depends on.
I just modified the local code that can be found in ./node_modules/preact-side-effect/lib/index.js
. One log statement in componentWillMount
and one in componentWillUnmount
. The one in componentWillMount
also logs the new count (it is keeping track of a stack of mounted instances). Here is the code for completeness:
SideEffect.prototype.componentWillMount = function componentWillMount() {
console.info("SideEffect.componentWillMount: mountedInstances.length=", mountedInstances.length + 1);
mountedInstances.push(this);
emitChange();
};
SideEffect.prototype.componentWillUnmount = function componentWillUnmount() {
console.info("SideEffect.componentWillUnmount");
var index = mountedInstances.indexOf(this);
mountedInstances.splice(index, 1);
emitChange();
};
The weird thing is that at the start of a new test, if I call SideEffect.peek()
, it still gives me the state from the last mounted instance. And indeed I see in the logs that at this point, componentWillUnmount
has not been called yet (even though unmountComponentAtNode
has). If I then proceed to render again in this new test, componentWillUnmount
is called just before the new node is mounted. Here is the log output:
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√tags without 'src' will not be accepted
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillUnmount'
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillMount: mountedInstances.length=', 1
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillMount: mountedInstances.length=', 2
√will set script tags based on deepest nested component
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillUnmount'
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillUnmount'
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillMount: mountedInstances.length=', 1
√sets undefined attribute values to empty strings
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√won't render tag when primary attribute (src) is null
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√won't render tag when primary attribute (innerHTML) is null
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
noscript tags
√can update noscript tags
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√will clear all noscripts tags if none are specified
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√tags without 'innerHTML' will not be accepted
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√won't render tag when primary attribute is null
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
style tags
√can update style tags
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√will clear all style tags if none are specified
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√tags without 'cssText' will not be accepted
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√won't render tag when primary attribute is null
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
misc
√throws in rewind() when a DOM is present
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√lets you read current state in peek() whether or not a DOM is present
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√will html encode string
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√will not change the DOM if it is receives identical props
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
×will only add new tags and will preserve tags when rendering additional Helmet instances
AssertionError: expected { Object (linkTags) } to have a property 'metaTags'
at Context.<anonymous> (webpack:///lib/test/HelmetTest.js:9:77606 <- lib/test/HelmetTest.js:235:77628)
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√can not nest Helmets
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
√will recognize valid tags regardless of attribute ordering
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'unmountComponentAtNode: child is defined'
Chrome 56.0.2924 (Windows 10 0.0.0) INFO LOG: 'SideEffect.componentWillMount: mountedInstances.length=', 1
(sorry about the noise, that’s Karma)
The last test is the failing one. I’m showing all this logging because it shows how other tests (that are doing rendering) actually successfully mount/unmount… which makes this one test so weird. Also notice how the length
reported from componentWillMount
(which is the new length after mounting) is correct. Instances are not piling up. Also notice how componentWillUnmount
is called just before the new one is mounted, even though unmountComponentAtNode
has sometimes been called multiple times in between.
I have been staring at this for hours now and can’t figure out what is happening so a second pair of eyes would be really appreciated!
Issue Analytics
- State:
- Created 7 years ago
- Comments:12 (12 by maintainers)
Top GitHub Comments
Fixed in 8.
Let’s keep this issue open, but I’ll be fixing #538 first - after that’s merged we’ll check if this still needs separate work.