Bug report
The FormSpy constructor has a side-effect which, when using React.StrictMode or React async mode, causes a subscription memory leak.
React.StrictMode helps detect unexpected side effects within the render phase by invocating some (most) of the React lifecycle methods twice - this includes the constructor. (See here for details/clarification).
The FormSpy constructor runs the following (here):
this.subscribe(props, (state: FormState) => { ...
which in turn runs (here):
subscribe = (
{ subscription }: Props,
listener: (state: FormState) => void
) => {
this.unsubscribe = this.props.reactFinalForm.subscribe(
listener,
subscription || all
)
}
hence setting the this.unsubscribe class-member variable. Because the constructor is ran twice, the first instantiation's unsubscribe value is overwritten, and only the 2nd call's is ever executed in componentWillUnmount (here).
According to React's docs on async rendering, render phase lifecycles may be invoked more than once, and so ~should~ can not have side-effects.
Because the above methods might be called more than once, it鈥檚 important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state.
All render-phase lifecycle methods should be side-effect free, allowing for compatibility with React.StrictMode and React async rendering mode.
https://codesandbox.io/s/n1p24n90xj
It's a little difficult to "show" the issue, but this will do it.
John -> JohnaDoe -> Does)FormSpy has re-mounted, hence invocated componentWillUnmount (commit phase) once and the constructor (render phase) twice. One of the original mounting's subscriptions has been retained and not been unsubscribed from during unmounting.The more you re-mount the FormSpy, the more subscriptions are left over. Mount it 10 times (so the button says "Mounted 10 times") and you'll see "Values changed" 11 times on every field change (9 stale subscriptions which should have been removed, and 2 new subscriptions).
React.StrictMode, or any version with async rendering enabled)Although the codesandbox example is non-real-world, we hit this problem in a real-world scenario where our form expands revealing extra form Fields and FormSpys.
I am more than happy to work on this and PR it, but will obviously needs plenty of review. It will also be a breaking change, as the subscription (and subsequent onChange being called) will be moved from the constructor to componentDidMount
Note: this work will also make it React 17+ compliant.
I'm also trying to ensure all tests pass as they currently do, and only change / fix what absolutely requires it.
I'll PR from my fork within the week for discussion.
EDIT: Didn't realise I had so little free time. Still working on this, and hope to get it PR'd up soon.
@erikras (re https://github.com/final-form/react-final-form/pull/427#issuecomment-481193028)
I cannot reply to the PR, as it's locked to contributors and members.
Apologies if you took my comment on the PR as an offence/attack - I certainly didn't mean to.
I closed the PR as I was getting emails regarding merge conflicts on every other PR that was merged in, and I was going to re-open after I got time to raise a secondary, contrasting PR.
Thanks for raising and taking a look, regardless. I'll try to get some time to pick it up again, rebase, etc.
I'll also be raising a few issues and making a few PRs for react-final-form-hooks, as we're using extensively on my current project. (excellent job with final-form, by the way.)
In the mean time...
if (process.env.NODE_ENV !== 'production') {
const pattern = /is deprecated and will be removed in the next major version/;
const { warn } = console;
console.warn = function warnWithoutReactViolations(message, ...rest) {
if (!pattern.test(message)) {
warn(message, ...rest);
}
};
}
This should be fixed in v5. Reopen if not.
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.