Skip to content

Commit 4652fcc

Browse files
Merge pull request #733 from Workiva/saferendermanager-retention-workaround
SafeRenderManager retention workaround
2 parents 74c999a + ee8ab08 commit 4652fcc

File tree

4 files changed

+208
-146
lines changed

4 files changed

+208
-146
lines changed

lib/src/util/safe_render_manager/safe_render_manager.dart

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,39 @@ class SafeRenderManager extends Disposable {
5252
/// If not specified, a new div will be used.
5353
final Element mountNode;
5454

55+
final _contentRefObject = createRef<dynamic>();
56+
5557
/// The ref to the component rendered by [render].
5658
///
5759
/// Due to react_dom.render calls not being guaranteed to be synchronous.
5860
/// this may not be populated until later than expected.
59-
dynamic contentRef;
61+
dynamic get contentRef => _contentRefObject.current;
62+
63+
// This setter only exists for backwards compatibility purposes.
64+
@Deprecated('This ref should only be read, since it gets set by the rendered content')
65+
set contentRef(dynamic value) => _contentRefObject.current = value;
6066

6167
_RenderState _state = _RenderState.unmounted;
6268

6369
/// A list of [render] calls queued up while the component is in the process
6470
/// of rendering.
6571
List<ReactElement> _renderQueue = [];
6672

67-
SafeRenderManager({Element mountNode, this.autoAttachMountNode = false})
68-
: mountNode = mountNode ?? DivElement();
73+
/// A ref to `this`, allowing us to create closures with references to this object that can be "revoked",
74+
/// so that any trees retained before React clears out old children https://github.com/facebook/react/issues/16087
75+
/// don't retain a reference to this object.
76+
///
77+
/// In those cases, usually it's only SafeRenderManagerHelper/SafeRenderManager that get retained,
78+
/// potentially since SafeRenderManagerHelper's tryUnmountContent plus the actual unmount are enough for React to
79+
/// clear out old children.
80+
///
81+
/// This is mostly in place to ease automated memory testing by preventing SafeRenderManager and its LeakFlags from
82+
/// being retained; any content retained by React will still be able to flag leaks.
83+
final _selfRef = createRef<SafeRenderManager>();
84+
85+
SafeRenderManager({Element mountNode, this.autoAttachMountNode = false}) : mountNode = mountNode ?? DivElement() {
86+
_selfRef.current = this;
87+
}
6988

7089
/// Renders [content] into [mountNode], chaining existing callback refs to
7190
/// provide access to the rendered component via [contentRef].
@@ -97,15 +116,19 @@ class SafeRenderManager extends Disposable {
97116
if (autoAttachMountNode && !document.contains(mountNode)) {
98117
document.body.append(mountNode);
99118
}
119+
// Create a closure variable so we're not retaining a reference to `this` inside `ref`.
120+
final _selfRef = this._selfRef;
100121
react_dom.render((SafeRenderManagerHelper()
101-
..ref = _helperRef
122+
..ref = (ref) {
123+
_selfRef.current?._helperRef(ref);
124+
}
102125
..getInitialContent = () {
103126
final value = content;
104127
// Clear this closure variable out so it isn't retained.
105128
content = null;
106129
return value;
107130
}
108-
..contentRef = _contentCallbackRef
131+
..contentRef = _contentRefObject
109132
)(), mountNode);
110133
} catch (_) {
111134
_state = _RenderState.unmounted;
@@ -203,10 +226,6 @@ class SafeRenderManager extends Disposable {
203226
}
204227
}
205228

206-
void _contentCallbackRef(ref) {
207-
contentRef = ref;
208-
}
209-
210229
@override
211230
Future<Null> onDispose() async {
212231
var completer = Completer<Null>();
@@ -232,6 +251,9 @@ class SafeRenderManager extends Disposable {
232251

233252
await completerFuture;
234253

254+
// Prevent any closures retaining `_selfRef` from retaining `this`.
255+
_selfRef.current = null;
256+
235257
await super.onDispose();
236258
}
237259
}

lib/src/util/safe_render_manager/safe_render_manager_helper.dart

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,18 @@ part 'safe_render_manager_helper.over_react.g.dart';
1818

1919
/// A component that allows for safe unmounting of its single child by waiting for state changes
2020
/// sometimes queued by ReactJS to be applied.
21-
@Factory()
2221
UiFactory<SafeRenderManagerHelperProps> SafeRenderManagerHelper =
2322
// ignore: undefined_identifier
24-
_$SafeRenderManagerHelper;
23+
castUiFactory(_$SafeRenderManagerHelper);
2524

26-
@Props()
27-
class _$SafeRenderManagerHelperProps extends UiProps {
25+
mixin SafeRenderManagerHelperProps on UiProps {
2826
@requiredProp
2927
ReactElement Function() getInitialContent;
3028

31-
CallbackRef contentRef;
29+
dynamic contentRef;
3230
}
3331

34-
@State()
35-
class _$SafeRenderManagerHelperState extends UiState {
32+
mixin SafeRenderManagerHelperState on UiState {
3633
ReactElement content;
3734
}
3835

@@ -57,24 +54,6 @@ class SafeRenderManagerHelperComponent extends UiStatefulComponent2<SafeRenderMa
5754
render() {
5855
if (state.content == null) return null;
5956

60-
return cloneElement(state.content, domProps()..ref = chainRef(state.content, _contentRef));
57+
return cloneElement(state.content, domProps()..ref = chainRefs(state.content.ref, props.contentRef));
6158
}
62-
63-
void _contentRef(ref) {
64-
props.contentRef?.call(ref);
65-
}
66-
}
67-
68-
// AF-3369 This will be removed once the transition to Dart 2 is complete.
69-
// ignore: mixin_of_non_class, undefined_class
70-
class SafeRenderManagerHelperProps extends _$SafeRenderManagerHelperProps with _$SafeRenderManagerHelperPropsAccessorsMixin {
71-
// ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value
72-
static const PropsMeta meta = _$metaForSafeRenderManagerHelperProps;
73-
}
74-
75-
// AF-3369 This will be removed once the transition to Dart 2 is complete.
76-
// ignore: mixin_of_non_class, undefined_class
77-
class SafeRenderManagerHelperState extends _$SafeRenderManagerHelperState with _$SafeRenderManagerHelperStateAccessorsMixin {
78-
// ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value
79-
static const StateMeta meta = _$metaForSafeRenderManagerHelperState;
8059
}

0 commit comments

Comments
 (0)