-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
[DataGrid] Data source with editing #16045
base: master
Are you sure you want to change the base?
Conversation
Deploy preview: https://deploy-preview-16045--material-ui-x.netlify.app/ Updated pages: |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
@@ -160,8 +160,8 @@ describeSkipIf(isJSDOM)('<DataGrid /> - Data source', () => { | |||
|
|||
setProps({ | |||
paginationModel: { page: 1, pageSize: 10 }, | |||
sortModel: [{ field: 'name', sort: 'asc' }], |
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.
Adding the warning surfaced this hidden issue in the test, it was trying to set the sortModel
field to name
which was not there.
*/ | ||
unstable_onDataSourceError?: (error: Error, params: GridGetRowsParams) => void; | ||
unstable_onDataSourceError?: (error: GridDataSourceError) => void; |
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.
Breaking change
It's updated for a better DX for errors originating from different operations (fetch, update, etc.).
GridDataSourceError
is a custom error class extended from the Error
class, it adds params
object and appropriate methods to support different contexts (params) for different originating sources (currently we have getRows
and updateRow
, but this could be extended to let's say deleteRow
, addRow
, etc.)
The alternate would be to define different set of props for each use case:
getRows
: onDataSourceGetRowsError
updateRows
: onDataSourceUpdateRowsError
I personally like handling it at one place to have less API surface. Open to comments.
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 am also for having one prop to handle all errors. Errors can have different classes that extend some base class to keep the common stuff together and have specific where needed.
/** | ||
* Returns true if this error was caused by a fetch operation | ||
*/ | ||
isFetch(): this is GridDataSourceError & { params: T } { |
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.
Having methods on the error object is a bit unusual.
Did you consider something like this instead?
type DataSourceErrorParams =
| [{ type: 'fetchRows', message: string }, { page: number }]
| [{ type: 'updateRow', message: string }, { rowId: string }];
interface Props {
unstable_onDataSourceError: <T extends DataSourceErrorParams>(
...args: T
) => void;
}
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.
Thank you for the suggestion, I did actually try a similar approach, it does have its pros but I decided to not use it for a few reasons, which I'll state below.
For a code-comparison I prepared a drilled-down version here.
- Type safety: As you can notice, doing
if (error.isFetch())
automatically makes the Typescript infer theparams
type and no manual type assertion is needed. - Readability: In my personal opinion
error.isFetch()
is more readable/self-explanatory thanerror.type === 'fetchRows'
- Extensibility: Adding more methods won't require updating the typescript function signature.
- Better Debuggable (opinionated): Due to methods like
instanceof
available onError
and it's derived classes, one can differentiate it with other errors by doingerror instanceof GridDataSourceError
, which might improve debugging experience.
On a second thought, I think it makes more sense to introduce different error signatures for each error and keep relevant parameters in the error class in order to:
- Nice DX. Use the
instanceof
to determine the type of error,error instanceof DataSourceFetchRowsError
andparams
type is inferred automatically. (It also seems to be a widely used pattern for similar use-cases) - Keep the signature of the handler consistent.
- Open the possibility of future extensibility.
Something like this.
Wdyt?
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 am also not sure if I used an error object with these kind of methods before.
Differentiate them by class looks better. I would also go for both
Have different classes and have method
prop in the error so that devs can use whatever they like more.
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.
Method prop has a slight disadvantage of having to manually do type assertions, differentiating by class (second link in my above comment) looks most appropriate to me
*/ | ||
unstable_onDataSourceError?: (error: Error, params: GridGetRowsParams) => void; | ||
unstable_onDataSourceError?: (error: GridDataSourceError) => void; |
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 am also for having one prop to handle all errors. Errors can have different classes that extend some base class to keep the common stuff together and have specific where needed.
message: errorThrown?.message, | ||
operationType: 'updateRow', | ||
params: updateParams, | ||
cause: errorThrown, |
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.
maybe context
?
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.
context
should also be fine, I just wanted to be more aligned with the Error.cause
since it's description seemed to match our use-case.
It is used when catching and re-throwing an error with a more-specific or useful error message in order to still have access to the original error.
Wdyt?
props.unstable_onDataSourceError( | ||
new GridDataSourceError({ | ||
message: errorThrown?.message, | ||
operationType: 'updateRow', |
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.
operationType: 'updateRow', | |
method: 'updateRow', |
/** | ||
* Returns true if this error was caused by a fetch operation | ||
*/ | ||
isFetch(): this is GridDataSourceError & { params: T } { |
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 am also not sure if I used an error object with these kind of methods before.
Differentiate them by class looks better. I would also go for both
Have different classes and have method
prop in the error so that devs can use whatever they like more.
return; | ||
} | ||
updatedRows[rowIndex] = rowUpdate; | ||
cache.set(getRowsParams, { ...cachedData, rows: updatedRows }); |
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.
It is way more complicated than this 😬
This only works if sorting and/or filtering is not applied. Otherwise, you need to have this row in some other place or not in this cache key at all anymore. Not only that, but it affects other cache keys as well.
I am not event sure if we can do anything to keep the cache once sort/filter is there unless we don't change the current cache structure completely.
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.
A composite key may be?
Instead of 'row.id' only, we create a key for all the differentiating factors e.g.
const key = JSON.stringify([row.id, filterModel, sortModel, groupKeys])
const value = getRowParams
Then, when searching for a specific row, we generate that composite key again based on the current model values.
I'll give it a try.
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.
The problem is the position
If you sort by certain column and you change the value of that column, you need to update all chunks in between and move one record up and then inject updated record in the chunk which holds the position that the new value occupies.
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.
Depending on how complex you want to go I see two possibilities with the current cache structure.
- You only keep cache chunks with the pagination param only (since it doesn't affect position or other data) and clear all others
- You track which columns have changed and you update all cache chunks that have sort/filter/group/aggregation params that are not related to the updated columns and clear the rest
As mentioned above, updating cache for the columns that are updated is pretty hard. Problem mentioned for sorting applies for other operations that we cache - you need new aggregation amount or you need to remove the row completely for some filter param because it becomes filtered out (which means you have to adjust some other chunks related to that filter param), etc.
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.
You track which columns have changed and you update all cache chunks that have sort/filter/group/aggregation params that are not related to the updated columns and clear the rest
@arminmeh You made some great points. I agree that delving deeper into this approach could be highly complex and performance-intensive. More importantly, Data Grid cache should not handle business logic owned by the server, such as server-side sorting, especially since it also lacks access to all the data.
If we extract the models which might affect the visibility or positioning of rows when an edit is made, they would be:
Model | Reasoning |
---|---|
sortModel |
Determines row positions; an edit might cause a row to move, requiring a refetch to maintain correct order. The edited row could literally be anywhere, even in the un-fetched chunks. |
filterModel |
Controls row visibility; an edited row may no longer match the filter criteria and needs to be removed or added dynamically. |
aggregationModel |
Affects aggregated values; editing a row could change totals, averages, or other computed metrics, necessitating a refresh. |
If any of these models are active, the Data Grid cannot accurately determine the edited row’s position without access to server-side business logic and data, which it does not have. Additionally, it cannot predict which row, if any, should replace a removed one in the current chunk.
The best option I see here is a hybrid approach where we partially reap the benefits of caching with editing.
-
✅ On every edit operation, if no sorting, filtering, or aggregation is active → Mutate cache entry
The users fully enjoy the client-side caching benefits in this case -
❌ If any of those models are active → Invalidate cache and refetch the current viewport
This case would be one of the limitations of the client-side cache, i.e. as soon as an edit is made, the cache has to be cleared. -
💡 As an alternative, we could urge the users to consider server-side caching in cases where editing has to be used with one or more of sorting, filtering, and aggregation models, since the server has access to all the data and could provide a more reliable caching.
Lmk how you feel about this approach?
@@ -125,6 +132,9 @@ export const useGridDataSourceBase = <Api extends GridPrivateApiCommunity>( | |||
const cacheResponses = cacheChunkManager.splitResponse(fetchParams, getRowsResponse); | |||
cacheResponses.forEach((response, key) => { | |||
cache.set(key, response); | |||
response.rows.forEach((row) => { | |||
rowIdToGetRowsParams.current[row.id] = fetchParams; |
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.
one row will be present in multiple cache keys when sorting/filtering/grouping is applied
Co-authored-by: Armin Mehinovic <[email protected]> Signed-off-by: Bilal Shafi <[email protected]>
Resolves #13261
Preview
In progress items:
useMockServer
Future improvements:
rowIdToGetRowsParams
touseTransition
or similar delayed computation mechanism (Should we ?)