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

Make it easier to prevent unnecessary DataTable re-renders #1357

Merged
merged 2 commits into from
Nov 9, 2023

Conversation

robertknight
Copy link
Member

@robertknight robertknight commented Nov 9, 2023

While adding some new UI to the VitalSource chapter/page selection dialog (see hypothesis/lms#5808) I found that the UI got very slow when working with a book that had a large (> 1000) number of entries in the chapter list. Such a large chapter list could really use virtualization to limit rendering time, but memo-ing is much easier to add and still has a significant impact.

I tried memo-ing the DataTable but that initially had no effect, due to the parent Scroll triggering a re-render even if the DataTable was memoed. This PR fixes that issue, and adds a bunch of additional memo-ing inside DataTable to make it easier for downstream apps to prevent re-renders. They now need to just memo: 1) The rows, 2) the columns, 3) the selected item. For better results they can also memo the whole DataTable.

In the process I imported the useStableCallback hook from Via since it is generally useful for these kinds of optimizations and I expect it will be useful in other applications in future.

This hook is useful for wrapping callbacks to avoid unnecessary re-renders due
to callback props changing. See hypothesis/via#1129.
 - Memo context provider values in various components. Otherwise
   every render of the component containing the `*.Provider` will cause
   any descendant consumers to re-render, even if the descendant has
   been memoed.

   Before this change, JSX like the following would result in a
   `DataTable` being re-rendered on every `Container` render:

   ```
   function Container() {
     const table = useMemo(() => <DataTable ... />, [...]);
     return <Scroll>{table}</Scroll>;
   }
   ```

   This happened because the `Scroll` component contained a context
   provider that was consumed by the `DataTable` and thus every render
   of the `Scroll` would re-render the `DataTable` child.

 - Memo rendered `DataTable` rows such that they only re-render if one
   of the following changes:

    - The row content
    - The columns
    - A custom `renderItem` callback
    - The selected row

   Users of `DataTable` can then more easily eliminate re-renders of
   large tables by memoing these props/callbacks.
@@ -49,6 +50,10 @@ export type DataTableProps<Row> = CompositeProps &
ComponentProps<Row> &
Omit<JSX.HTMLAttributes<HTMLElement>, 'size' | 'rows' | 'role' | 'loading'>;

function defaultRenderItem<Row>(r: Row, field: keyof Row): ComponentChildren {
return r[field] as ComponentChildren;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Hoisted this default prop value to a top-level function so it doesn't change on each render.

onConfirmRow?.(row);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Wrap callback props so we don't unnecessarily re-render even if consumers fail to useCallback these.

@@ -0,0 +1,39 @@
// These tests use Preact directly, rather than via Enzyme, to simplify debugging.
Copy link
Member Author

Choose a reason for hiding this comment

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

This file and use-stable-callback.ts were copied from Via.

@robertknight robertknight requested a review from acelaya November 9, 2023 14:14
Copy link
Contributor

@acelaya acelaya left a comment

Choose a reason for hiding this comment

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

Good one 👍🏼

robertknight added a commit to hypothesis/lms that referenced this pull request Nov 9, 2023
This avoids an unnecessary re-render of the DataTable if the chapters haven't
changed, which is important if the book has a lot of pages or chapters. For this
change to work requires hypothesis/frontend-shared#1357.
robertknight added a commit to hypothesis/lms that referenced this pull request Nov 9, 2023
This avoids an unnecessary re-render of the DataTable if the chapters haven't
changed, which is important if the book has a lot of pages or chapters. For this
change to work requires hypothesis/frontend-shared#1357.
@robertknight robertknight merged commit b389fd5 into main Nov 9, 2023
2 checks passed
@robertknight robertknight deleted the datatable-optimizations branch November 9, 2023 15:59
robertknight added a commit to hypothesis/via that referenced this pull request Nov 10, 2023
…frontend-shared

This hook was copied from Via into the frontend-shared package in
hypothesis/frontend-shared#1357.
robertknight added a commit that referenced this pull request Nov 10, 2023
robertknight added a commit that referenced this pull request Nov 10, 2023
robertknight added a commit to hypothesis/via that referenced this pull request Nov 10, 2023
…frontend-shared

This hook was copied from Via into the frontend-shared package in
hypothesis/frontend-shared#1357.
robertknight added a commit to hypothesis/via that referenced this pull request Nov 10, 2023
…frontend-shared

This hook was copied from Via into the frontend-shared package in
hypothesis/frontend-shared#1357.
robertknight added a commit to hypothesis/via that referenced this pull request Nov 13, 2023
…frontend-shared

This hook was copied from Via into the frontend-shared package in
hypothesis/frontend-shared#1357.
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.

2 participants