Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPLAT-7979: Add SafeRenderManager #390

Merged
merged 5 commits into from
Oct 29, 2019
Merged

Conversation

greglittlefield-wf
Copy link
Contributor

@greglittlefield-wf greglittlefield-wf commented Oct 21, 2019

Motivation

There were some cases where top-level react_dom.render/.unmountComponentAtNode calls were being done from within the event handlers and lifecycle methods of React components, which encountered timing-related issues in React 16.

Changes

  • Add SafeRenderManager:

A class that manages the top-level rendering of a ReactElement into a given node,
with support for safely rendering/updating via render and safely unmounting via tryUnmount.

Content is also unmounted when this object is disposed.

This is useful in cases where react_dom.render or react_dom.unmountComponentAtNode
may or may not be called from React events or lifecycle methods, which can have
undesirable/unintended side effects.

For instance, calling react_dom.unmountComponentAtNode can unmount a component
while an event is being propagated through a component, which normally would never happen.
This could result in null errors in the component as the event logic continues.

SafeRenderManager uses a helper component under the hood to manage the rendering of content
via Component state changes, ensuring that the content is mounted/unmounted as it
normally would be.

  • Add tests
  • Adjust two lints

Release Notes

  • Add SafeRenderManager, a class that manages the top-level rendering of a ReactElement into a given node, with support for safely rendering, updating, and unmounting content.

Review

See CONTRIBUTING.md for more details on review types (+1 / QA +1 / +10) and code review process.

Please review:

QA Checklist

  • Tests were updated and provide good coverage of the changeset and other affected code
  • Manual testing was performed if needed
    • Steps from PR author:
      • Testing branches pass when this is pulled in
    • Anything falling under manual testing criteria outlined in CONTRIBUTING.md

Merge Checklist

While we perform many automated checks before auto-merging, some manual checks are needed:

  • A Client Platform member has reviewed these changes
  • There are no unaddressed comments - this check can be automated if reviewers use the "Request Changes" feature
  • For release PRs - Version metadata in Rosie comment is correct

@aviary2-wf
Copy link

Security Insights

No security relevant content was detected by automated scans.

Action Items

  • Review PR for security impact; comment "security review required" if needed or unsure
  • Verify aviary.yaml coverage of security relevant code

Questions or Comments? Reach out on Slack: #support-infosec.

@aaronlademann-wf aaronlademann-wf added this to the 3.0.0 milestone Oct 21, 2019
Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Just a couple of minor things.

@greglittlefield-wf greglittlefield-wf changed the title Add SafeRenderManager and preliminary tests Add SafeRenderManager Oct 24, 2019
@rmconsole7-wk rmconsole7-wk changed the title Add SafeRenderManager CPLAT-7979 Add SafeRenderManager and preliminary tests Oct 24, 2019
@greglittlefield-wf greglittlefield-wf changed the title CPLAT-7979 Add SafeRenderManager and preliminary tests CPLAT-7979: Add SafeRenderManager Oct 29, 2019
@greglittlefield-wf greglittlefield-wf marked this pull request as ready for review October 29, 2019 03:19
Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @greglittlefield-wf !!! All the shared tests are particularly detailed, which should provide consumers with great confidence to integrate this into their libs.

Aside from a few suggestions / questions... The copyright stuff needs to be added to the top of all the new files:

// Copyright 2019 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

Comment on lines +162 to +171
// Don't do this in a finally since onMaybeUnmounted can get called async.
callOnMaybeUnmounted(true);
rethrow;
}
} else {
try {
_unmountContent();
} finally {
callOnMaybeUnmounted(true);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the comment on line 162 apply to line 170?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, since there isn't a onMaybeUnmounted callback being used here.


void tryUnmountContent({void onMaybeUnmounted(bool isUnmounted)}) {
setState(newState()..content = null, () {
onMaybeUnmounted?.call(state.content == null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity - is it possible for state.content to be non-null within a callback of a call to setState that sets state.content to null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; this can happen if this setState is batched with another setState that sets it to something else.

expect(renderManager.mountNode.parent, document.body);

renderManager.tryUnmount();
expect(renderManager.mountNode.parent, isNull);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is probably kosher - but when I first read this - I thought... it... null'd document.body??!!?!!?

Not saying you have to change it, but something like this would test the same thing:

expect(document.body.contains(renderManager.mountNode), isFalse);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had this exact thing at first, but then changed it to what I have now so that the failure messages would be more helpful.

Expected: null
Actual: BodyElement

vs

Expected: false
Actual: true

Comment on lines +134 to +135
renderManager.render(Dom.div()());
expect(renderManager.mountNode.parent, isNull);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If mountNode is not specified, and autoAttachMountNode is false... isn't that an invalid configuration? What would it render into?

I realize this test is not really testing that - I'm just saying that the conditions of this test made me think this probably shouldn't be supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's a valid configuration. The manager defaults to a new, empty div if one isn't specified, and the consumer would be responsible for attaching/detaching it from the DOM.

}

if (isThrowingTest) {
expect(mountNode.text, isEmpty, reason: 'test setup check: React should have unmounted throwing component tree');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we just remove this logic when we port this change into 2.x stable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good to keep in as a sanity check

}
)('setup render'));

// Use a real click since simulated clicks don't trigger this async behavior
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to alpha release... feedback will be addressed in a follow-up PR.

@Workiva/release-management-pp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants