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

fix: infinite re-render on reoccuring ids #7657

Merged
merged 9 commits into from
Feb 4, 2025
Merged

Conversation

nwidynski
Copy link
Contributor

Closes #7655

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@nwidynski nwidynski marked this pull request as draft January 23, 2025 20:13
@nwidynski nwidynski marked this pull request as ready for review January 23, 2025 22:57
Copy link
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

Thanks for diagnosing and fixing this, what a tricky little bug

@nwidynski
Copy link
Contributor Author

@LFDanLu Can we get an additional pair of eyes on this? Afaik, should be good to go 👍

Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

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

Thanks for this!

@reidbarber reidbarber added this pull request to the merge queue Feb 4, 2025
Merged via the queue into adobe:main with commit a861ca4 Feb 4, 2025
30 checks passed
let result: Props = {...args[0]};
for (let i = 1; i < args.length; i++) {
let result: Props = {...args[args.length - 1]};
for (let i = args.length - 2; i >= 0; i--) {
Copy link
Member

@snowystinger snowystinger Feb 17, 2025

Choose a reason for hiding this comment

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

Following up on this, I ran a benchmark in jest with this file's set of changes. It is very dependent on the objects being merged and their order.

As it was: 9876 ms
With changes: 10754 ms


describe('mergeProps', function () {
  it('benchmark', function () {
    let startTime = performance.now();
    for (let i = 0; i < 1000000; i++) {
      mergeProps(...data);
    }
    let endTime = performance.now();
    console.log('mergeProps', endTime - startTime);
  });
});

// many objects to merge with complex values and overlapping keys
let data = [{
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  e: 5,
  f: 6,
  g: 7,
  h: 8,
  i: 9,
  j: 10,
  id: 'foo',
}, {
  h: 80,
  i: 90,
  j: 100,
  onClick: () => {},
  onMouseEnter: () => {},
  UNSAFE_className: 'foo',
  UNSAFE_style: { color: 'red' },
  id: 'bar',
},
{
  a: 10,
  b: 20,
  c: 30,
  d: 40,
  e: 50,
  f: 60,
  g: 70,
  h: 80,
  i: 90,
  j: 100,
  id: 'baz',
},
{
  a: 10,
  b: 20,
  c: 30,
  d: 40,
  e: 50,
  f: 60,
  g: 70,
  h: 80,
  i: 90,
  j: 100,
  id: 'qux',
},
{
  onClick: () => {},
  UNSAFE_className: 'bar',
  UNSAFE_style: { color: 'yello' },
  a: 10,
  b: 20,
  c: 30,
  d: 40,
  e: 50,
  id: 'quux',
},
{
  onFoo: () => {},
  UNSAFE_className: 'baz',
  UNSAFE_style: { color: 'green', display: 'block', position: 'absolute', top: 0, left: 0 },
  a: 100,
},
{
  onBar: () => {},
  UNSAFE_className: 'qux',
  UNSAFE_style: { color: 'blue', display: 'none', position: 'relative', bottom: 0, right: 0 },
  b: 200,
},
{
  onBaz: () => {},
  UNSAFE_className: 'quux',
  UNSAFE_style: { color: 'purple', display: 'flex', position: 'fixed', top: 0, right: 0 },
  c: 300,
},
{
  onQux: () => {},
  UNSAFE_className: 'quuz',
  UNSAFE_style: { color: 'pink', display: 'grid', position: 'sticky', bottom: 0, left: 0 },
  d: 400,
},
{
  onQuux: () => {},
  UNSAFE_className: 'quuz',
  UNSAFE_style: { color: 'orange', display: 'inline', position: 'static', bottom: 0, right: 0 },
  e: 500,
}];

but if I change the order of the objects being merged by using data = data.reverse()

As it was: 12110 ms
With changes: 7623 ms

Copy link
Member

Choose a reason for hiding this comment

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

We'd need to check for actual usage in an application or benchmark our actual components, maybe TableView or something for more accurate measurements

Copy link
Member

Choose a reason for hiding this comment

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

Ok, tried it in our RAC Table tests using

  it('should allow selection even when drag and drop is disabled', async () => {
    let startTime = performance.now();
    for (let i = 0; i < 1000; i++) {
      let {unmount} = render(
        <DraggableTableWithSelection />
      );
      unmount();
    }
    let endTime = performance.now();
    console.log('Duration:', endTime - startTime);
  });

As it was: 12063 ms
With changes: 12300 ms

So, it's likely slower in real life.

Copy link
Contributor Author

@nwidynski nwidynski Feb 17, 2025

Choose a reason for hiding this comment

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

Interesting, I guess this largely depends on where large objects are expected in the array, since the initial spread is doing most of the heavy lifting here.

I would have naively assumed them to occur in reverse order, as the first array item is often times user props while the later are usually hook props. I guess this is made obsolete by all the hooks often times just overriding a small portion of another hooks props, thereby favoring the prior version.

Thanks for giving this proper testing 👍

Copy link
Member

Choose a reason for hiding this comment

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

yeah, I'm still not entirely convinced by one component, even if it's a fairly complex one. Given that one ordering gave a 1sec delta and the reverse gave a 5sec delta in that test, I'd love to see if we can't benefit from that better delta. But until we can come up with a way to test across all components with likely prop objects being added... this will do.

Thanks again for all the work on it. 🔥

@nwidynski nwidynski deleted the id-fix branch February 21, 2025 09:57
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.

Duplicated ids in mergeProps causing infinte re-render in strict mode
3 participants