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(DataTable): sort table on initial render with initialSortColumn or initialSortDirection props #4602

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

bwittenberg
Copy link
Contributor

@bwittenberg bwittenberg commented May 18, 2024

Closes #3951

Changelog

New

Changed

  • The useTable hook is responsible to sort the data for the DataTable. If the data changes, then it sorts according to the current sort state. It should also sort data on initial render. This PR updates the initial state of useTable so that the data is sorted on initial render.
  • DRY up the test by extracting functions that read headers and rows.
  • Add Jest tests

Removed

Rollout strategy

Answering this question led to a questions about the purpose/intended use of this component. In my experience with data tables, consumers either have all the data on the client and want to render it in the table, or consumers do not have all the data, and may need to fetch more.

If consumers have all the data, then it's possible to sort and filter on the client, and then render correct results. I'll name this "completeData" mode. I wouldn't use this name as part of the API, but it works for this discussion.

If consumers do not have all the data, then it's not possible to sort and filter on the client, because all client side sorts and filters operate on a partial data set. For correct results, the sort and filter must be done on the server, where the complete data set is known. I'll name this "partialData" mode.

Why discuss these two different usages in this PR?

Prior to this change, on initial render only, the data table would not sort the passed in data. For consumers with partial data, this is the desired behavior. After this change, consumers that are using this component with partial data will notice that the data is now sorted on the client on initial render.

Based on the current DataTable API, I think this component was designed for "completeData" mode, because any sorting and renders after the initial render only sort the data that's in the component state.

To support "partialData" mode, this component would need to allow consumers to pass in the "sortColumn" and "sortDirection", and also expose callbacks so consumers can respond to sort change like onSortChange.

To handle the "partialData" and "completeData" modes of a table, it could make sense to build a "controlled" version of the DataTable, maybe something like StatelessDataTable, and then the DataTable can remain the "uncontrolled" version that adds state to the StatelessDataTable.

After looking at more of the components in DataTable, for the "partialData" mode, consumers could use Table directly with import { Table } from '@primer/react/experimental'.

Apologies for the lengthy discussion. I suspect that the authors already know all of this, so this is mostly me trying to convince myself that this change should be considered a patch, because the DataTable is designed for the "completeData" mode, and consumers are probably not expecting the buggy behavior.

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

  • Visit http://<storybookurl>/?path=/story/drafts-components-datatable-features--with-custom-sorting, assert data is sorted by updated at, click the updated at column, assert data is sorted in opposite direction, click Repository header, assert data is sorted by Repository, click Repository again, assert sort changes direction

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome (version 124.0.6367.207)
  • Tested in Firefox (version 126.0)
  • Tested in Safari (version 17.4.1)
  • Tested in Edge (version 124.0.2478.109)
  • (GitHub staff only) Integration tests pass at github/github (Learn more about how to run integration tests)

@bwittenberg bwittenberg requested a review from a team as a code owner May 18, 2024 16:27
Copy link

changeset-bot bot commented May 18, 2024

🦋 Changeset detected

Latest commit: 3370857

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@TylerJDev
Copy link
Contributor

Hello @bwittenberg, thank you for the PR! Also thank you for providing detailed comments on what this PR aims to achieve.

The one potential issue that could arise from this change, is that that the state would render twice, as the conditional if (data !== prevData) would always equal true upon initial render as data is null, causing a re-render with state being updated within that conditional. This seems fine, but I wonder if we could sort the data initially when we initialize rowOrder, (e.g. sorting the data within useState). This way, the data will always be sorted on initial render, and we don’t need to worry about a double re-render. What do you think about this approach?

For example:

const [rowOrder, setRowOrder] = useState((data) => {
 // sort the data when we store it in state
})

@bwittenberg
Copy link
Contributor Author

@TylerJDev , thanks for the review! I'm happy to implement your suggestion, but wanted to provide some thoughts for you to consider. Please let me know which option you prefer, or maybe you have another option in mind that I'm not seeing. If you prefer changes to the original PR, I can run through the PR checklist again.

FWIW, I prefer Option3, but again, happy to follow your guidance.

Option1: Avoid the call to setState on initial render (like you suggested)

I implemented this suggestion here: https://github.com/bwittenberg/primer-react/pull/1/files

Pros: Improves initial render performance because the hook will only run once.
Cons: More code changes, so there's a higher risk of bugs.

Option2: Keep call to setState on initial render (no changes to PR)

Pros: Least amount of code changes.
Cons: Less performant than Option1

Although this is less performant than Option1, it's unclear to me how much less performant it will be. This should have similar performance characteristics to updates when the data prop changes.I did some testing with breakpoints and discovered that while React will not "render" the children twice, it will call the DataTable component function twice, which iterates over the headers and rows.

The React docs discuss the technique of calling set state during render in the "Storing information from previous render" section in the docs, and React has an optimization to avoid an extra render.

This pattern can be hard to understand and is usually best avoided. However, it’s better than updating state in an effect. When you call the set function during render, React will re-render that component immediately after your component exits with a return statement, and before rendering the children. This way, children don’t need to render twice. The rest of your component function will still execute (and the result will be thrown away). If your condition is below all the Hook calls, you may add an early return; to restart rendering earlier.

I added a performance test to confirm the render count. I'd probably keep the test because it adds a little bit of value, and seems easy to maintain.

Here's an example of an early return that would improve performance on initial render and when certain props change, but I wouldn't advocate for this change, because I suspect it's a little known optimization technique and feels brittle: https://github.com/bwittenberg/primer-react/pull/3/files

Option3: Remove the redundant state

Example PR: https://github.com/bwittenberg/primer-react/pull/2/files

Pros: Improves performance for initial render and when props change. Removes redundant state and improves readibility.
Cons: More code changes, so there's a higher risk of introducing bugs.

This option removes the rowOrder and prevData states, because they are not necessary for this hook to return sorted rows. Removing unnecessary state is recommended in the React docs

If the value you need can be computed entirely from the current props or other state, remove that redundant state altogether. If you’re worried about recomputing too often, the useMemo Hook can help.

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.

Alphanumeric sorting initial sort
2 participants