-
Notifications
You must be signed in to change notification settings - Fork 4
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
refactor: optimize re-renders #87
refactor: optimize re-renders #87
Conversation
🚀 Deployed on https://pr-87--dhis2-data-entry.netlify.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review
Great work @Birkbjo 🦾
I ran the app locally with my CPU throttled 4x. It is already much better than it was, and we still can make more performance optimisations in other areas (i.e. not resetting the initial values). So I'm feeling quite confident we can get this app optimised without having to change too much.
I left a few minor comments. Looking forward to finding out what you think about them.
Side note
When I tried the Reproductive Health form (which is quite a large form, so I tend to use it for performance checks), I did notice something strange though....
There are some empty cells at the end of the table, which don't seem to be needed....
The old app doesn't have this issue so I guess we have a bug in our category-combo-table. Shall we try to fix this in the current PR, or shall I open a JIRA ticket for it?
categories, | ||
}) => { | ||
const { data: metadata } = useMetadata() | ||
const { deId: activeDeId, cocId: activeCocId } = useActiveCell() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking that it would probably benefit the app's architecture and performance if we were to push this form state subscription down to the level of the actual header cell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really think it's necessary to push it further. You could in theory have a CategoryTableHeaderColumns
and move it there, and basically move the rendered component from columns.map...
. But that would only save re-rendering of the static components "Category name" and "PaddingCell". Again these are static, with no dynamic props and no DOM-updated are needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was mainly concerned about the nested iterations needed to compute the columns, and that fact that they would need to run whenever a the active cell changes. I suggested solving this by pushing the subscription to the active cell down, but I noticed you are now memoizing these columns in the useCategoryColumns
. I think that's an equally valid solution.
// Is the active cell in this cat-combo table? Check to see if active | ||
// data element is in this CCT | ||
const isThisTableActive = dataElements.some(({ id }) => id === activeDeId) | ||
|
||
// Find if this column header includes the active cell's column | ||
const isHeaderActive = (headerIdx, headerColSpan) => { | ||
const activeCellColIdx = categoryOptionCombos.findIndex( | ||
(coc) => activeCocId === coc.id | ||
) | ||
const idxDiff = activeCellColIdx - headerIdx * headerColSpan | ||
return isThisTableActive && idxDiff < headerColSpan && idxDiff >= 0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just wondering why we also need to isThisTableActive
const. Is it possible that a form contains several tables with the same dataElements?
Look at this very naively, I would expect that every cell will be unique so we don't need this variable per se. If you have added that isThisTableActive
variable to make the code more performant, then you should add it as an early return in the isHeaderActive
function.
Having said all of that, if you agree with my earlier suggestion, about moving the active/inactive logic down to the actual header cell, then this code is going to change anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some refactoring to not need to pass all dataElements
to this component. So I pass a function checkTableActive
instead. But yes, it's needed because section-forms render multiple tables, and we only want to highlight the active table.
Is it possible that a form contains several tables with the same dataElements?
No, and that's why this fact is used to check if the current table is "active" or not.
then you should add it as an early return in the isHeaderActive function.
I don't really understand what you mean here? This is only a part of the check isHeaderActive
. It's not enough to know if the current table is active. You need to also check if the current active cell is in the correct columns AND row as well. This code is for highlighting of the headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some refactoring to not need to pass all
dataElements
to this component. So I pass a functioncheckTableActive
instead. But yes, it's needed because section-forms render multiple tables, and we only want to highlight the active table.Is it possible that a form contains several tables with the same dataElements?
No, and that's why this fact is used to check if the current table is "active" or not.
Partly from your explanation above, and by looking at the code a bit more closely, I get this now 👍 .
then you should add it as an early return in the isHeaderActive function.
I don't really understand what you mean here?
I was mainly just figuring out what was happening, and my suggestion about returning early was basically just this (see the comment):
const isHeaderActive = (headerIdx, headerColSpan) => {
if (!checkTableActive(activeDeId)) {
return false
}
// by returning early you can avoid this iteration
const activeCellColIdx = categoryOptionCombos.findIndex(
(coc) => activeCocId === coc.id
)
const idxDiff = activeCellColIdx - headerIdx * headerColSpan
return (idxDiff < headerColSpan && idxDiff >= 0)
}
But this is very much a micro-optimisation, so I'm very happy to skip this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying some things all good now 👍
Reg. this:
The old app doesn't have this issue so I guess we have a bug in our category-combo-table. Shall we try to fix this in the current PR, or shall I open a JIRA ticket for it?
Let's just defer this, since it's unrelated to your current work. I created TECH-1255 for this.
categories, | ||
}) => { | ||
const { data: metadata } = useMetadata() | ||
const { deId: activeDeId, cocId: activeCocId } = useActiveCell() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was mainly concerned about the nested iterations needed to compute the columns, and that fact that they would need to run whenever a the active cell changes. I suggested solving this by pushing the subscription to the active cell down, but I noticed you are now memoizing these columns in the useCategoryColumns
. I think that's an equally valid solution.
// Is the active cell in this cat-combo table? Check to see if active | ||
// data element is in this CCT | ||
const isThisTableActive = dataElements.some(({ id }) => id === activeDeId) | ||
|
||
// Find if this column header includes the active cell's column | ||
const isHeaderActive = (headerIdx, headerColSpan) => { | ||
const activeCellColIdx = categoryOptionCombos.findIndex( | ||
(coc) => activeCocId === coc.id | ||
) | ||
const idxDiff = activeCellColIdx - headerIdx * headerColSpan | ||
return isThisTableActive && idxDiff < headerColSpan && idxDiff >= 0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some refactoring to not need to pass all
dataElements
to this component. So I pass a functioncheckTableActive
instead. But yes, it's needed because section-forms render multiple tables, and we only want to highlight the active table.Is it possible that a form contains several tables with the same dataElements?
No, and that's why this fact is used to check if the current table is "active" or not.
Partly from your explanation above, and by looking at the code a bit more closely, I get this now 👍 .
then you should add it as an early return in the isHeaderActive function.
I don't really understand what you mean here?
I was mainly just figuring out what was happening, and my suggestion about returning early was basically just this (see the comment):
const isHeaderActive = (headerIdx, headerColSpan) => {
if (!checkTableActive(activeDeId)) {
return false
}
// by returning early you can avoid this iteration
const activeCellColIdx = categoryOptionCombos.findIndex(
(coc) => activeCocId === coc.id
)
const idxDiff = activeCellColIdx - headerIdx * headerColSpan
return (idxDiff < headerColSpan && idxDiff >= 0)
}
But this is very much a micro-optimisation, so I'm very happy to skip this.
I hadn't seen your last comment yet when I approved, and was creating a new JIRA issue for the empty columns.....
I'll make sure to add this comment to the JIRA ticket, but will actually keep the JIRA ticket open. Even though it may be a backend issue, the old app deals with these types of misconfigurations gracefully and we don't. So I'm guessing we will need to fix it somewhere. |
Ah, I understand now! Yeah we could indeed do that, but yeah probably very much a micro-optimisation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code generally looks good! Despite approving (as it seems everything works as expected), I left one change request regarding abbreviations.
I have one question for my own understanding: Why is using two providers (one "getter provider" and one "setter provider") functionally different from using a single provider? As I understanding it, it does seem to make a different, I just don't understand why yet. Could you explain that? Maybe we can put the final explanation that we end up with as a comment in the current-item-provider.js
(where the two providers are being nested)
Actually, in regards to the provider thing, Dan Abramov said something about this: facebook/react#15156 (comment) Option 2 seems to be similar to something I've suggested before: The only component inside the form that needs the context is I guess as the provider stuff already works, I don't see a need to change that now though! Just wanted to mention that there seems to be a more "official" solution as it's coming from Dan, might be worth considering that solution if we end up in this situation again |
Thanks for the link, that's helpful! But our solution is his "option 1" and preferred solution, so I don't really see how "option 2" is "more official"? He explicitly marks our current solution as the preferred solution? We are splitting our context - the value that is changing often, and the setter-function that never changes. The other solutions will still cause re-renders. Yes, I guess they would be cheap - but they are still re-renders. So I have to say I prefer the current solution - even for the future. Of course, this is only needed if a value is subscribed to in a lot of components - this kind of optimisation shouldn't be that common. We are rendering the extra context-provider in the same |
You're right! I thought about this differently, I read this like "Split providers if they handle different things on a conceptual level", but of course, you're right, in our case the value and setter are not used together, so this makes perfect sense! Never mind me, then! |
Refactors the header in to a separate component. This PR is meant to make the
CategoryComboTable
-component a bit easier to reason about, as well as solve some re-renders.