FormSpy: re-mounting subscription memory leak from render-phase side-effects
See original GitHub issueAre you submitting a bug report or a feature request?
Bug report
What is the current behavior?
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’s 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.
What is the expected behavior?
All render-phase lifecycle methods should be side-effect free, allowing for compatibility with React.StrictMode
and React async rendering mode.
Sandbox Link
https://codesandbox.io/s/n1p24n90xj It’s a little difficult to “show” the issue, but this will do it.
- Open & clear the Console
- Add 1 letter to the end of either field (e.g.
John
->Johna
- Check the console and you’ll see “Values changed” twice - This is because the constructor was invocated twice, leading to 2 subscriptions.
- Click the “Re-mount FormSpy” button & clear the console
- Add 1 letter to the end of either field (e.g.
Doe
->Does
) - Check the console and you’ll see “Values changed” three times - The
FormSpy
has re-mounted, hence invocatedcomponentWillUnmount
(commit phase) once and theconstructor
(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).
What’s your environment?
- Final Form v4.11.0
- React-Final-Form v4.0.2
- React v16.5.2 (or any version with
React.StrictMode
, or any version with async rendering enabled)
Other information
Although the codesandbox example is non-real-world, we hit this problem in a real-world scenario where our form expands revealing extra form Field
s and FormSpy
s.
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
Issue Analytics
- State:
- Created 5 years ago
- Reactions:2
- Comments:6 (1 by maintainers)
Top GitHub Comments
This should be fixed in
v5
. Reopen if not.In the mean time…