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

Remove componentDidMount and componentDidUpdate hooks from <Popover> #6805

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

ab-pm
Copy link

@ab-pm ab-pm commented May 15, 2024

While using <Popover> in controlled mode to work around #6171, I noticed that it always renders twice when onInteraction changes my isOpen state. On first render, renderTarget is called with outdated arguments (closed instead of open), only immediately after in the second render it gets passed the correct values.
You can repro with something like

<Popover
	
	isOpen={isOpen}
	onInteraction={(nextOpenState, event) => {
		console.log('interaction', nextOpenState, event);
		setIsOpen(nextOpenState);
	}}
	renderTarget={props => {
		console.log('Rendering popover target', isOpen, props);
		return ;
	}}
/>

I traced the problem down to setState being called from componentDidUpdate. Instead the component should simply take the isOpen value from the props and ignore its local state altogether in controlled mode.

Checklist

  • Includes tests
  • Update documentation

Changes proposed in this pull request:

  • avoid calling setState from componentDidUpdate, which leads to the component rendering twice in controlled mode
  • instead, just ignore local state when the component is in controlled mode
  • remove componentDidMount and componentDidUpdate hooks from <Popover> altogether
  • assert that onInteraction is always called with an event
  • also remove hasDarkParent and isClosingViaEscapeKeypress from react component state, put them in class properties instead and update them whenever the overlay is opening/closing

Reviewers should focus on:

  • dark mode and esc key behavior
  • coding style (I'm unfamiliar with Bluprint guidelines)
  • testing (I've only done this in the web editor, haven't actually installed the dev environment and ran any tooling)
  • whether no longer calling onInteraction due to props change is a bugfix or a compatibility issue

@palantirtech
Copy link
Member

Thanks for your interest in palantir/blueprint, @ab-pm! Before we can accept your pull request, you need to sign our contributor license agreement - just visit https://cla.palantir.com/ and follow the instructions. Once you sign, I'll automatically update this pull request.

@changelog-app
Copy link

changelog-app bot commented May 15, 2024

Generate changelog in packages/core/changelog/@unreleased

What do the change types mean?
  • feature: A new feature of the service.
  • improvement: An incremental improvement in the functionality or operation of the service.
  • fix: Remedies the incorrect behaviour of a component of the service in a backwards-compatible way.
  • break: Has the potential to break consumers of this service's API, inclusive of both Palantir services
    and external consumers of the service's API (e.g. customer-written software or integrations).
  • deprecation: Advertises the intention to remove service functionality without any change to the
    operation of the service itself.
  • manualTask: Requires the possibility of manual intervention (running a script, eyeballing configuration,
    performing database surgery, ...) at the time of upgrade for it to succeed.
  • migration: A fully automatic upgrade migration task with no engineer input required.

Note: only one type should be chosen.

How are new versions calculated?
  • ❗The break and manual task changelog types will result in a major release!
  • 🐛 The fix changelog type will result in a minor release in most cases, and a patch release version for patch branches. This behaviour is configurable in autorelease.
  • ✨ All others will result in a minor version release.

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Remove componentDidMount and componentDidUpdate hooks from <Popover>

Check the box to generate changelog(s)

  • Generate changelog entry

@ab-pm ab-pm force-pushed the fix-popover-rerender branch 2 times, most recently from 7f8bad0 to a795ae7 Compare May 15, 2024 19:26
…ver>`

* avoid calling `setState` from `componentDidUpdate`, which leads to the component rendering twice in controlled mode
* instead, just ignore local state when the component is in controlled mode
* assert that `onInteraction` is always called with an event
* also remove `hasDarkParent` and `isClosingViaEscapeKeypress` from react component state, put them in class properties instead and update them whenever the overlay is opening/closing
@ab-pm
Copy link
Author

ab-pm commented May 22, 2024

@palantir do you need anything else? Do I need to provide a changelog entry before the PR even is reviewed?

@ericjeney
Copy link
Contributor

Nothing else necessary, and you can ignore the changelog check! We should be able to give this a review next week.

Copy link
Contributor

@gluxon gluxon left a comment

Choose a reason for hiding this comment

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

Thank you! This fix is very appreciated. I was also looking at #6771 recently, which was another case where we should have simply done a simple nullish coalesce like you did instead of complicated state handling.

return this.props.isOpen ?? this.state.isOpen;

Pure Rendering

I'm a bit nervous about the other changes in this PR that move the hasDarkParent and isClosingViaEscapeKeypress state booleans to be instance variables. I'm not sure if that change will work as expected with React 18's concurrent renderer, which expects the render output of a component to be a pure function of its state and props.

https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-double-rendering-in-development

React assumes that every component you write is a pure function. This means that React components you write must always return the same JSX given the same inputs (props, state, and context).

I acknowledge that you'll probably find existing Blueprint code that violates this best practice since it only became a stronger recommendation as of React 18. We shouldn't actively move away from this best practice in new code though.

Request

Could we revert the changes to hasDarkParent and isClosingViaEscapeKeypress for now? If there's factors I'm missing that make this safe to do, I'd be open to hear those explanations. Thanks.

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

Successfully merging this pull request may close these issues.

None yet

4 participants