-
-
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
[core] Each feature hook should initialize synchronously its state #2293
Comments
I would even say that it should fix: Regarding the options. I would personally try 1 first, see if it's flying, otherwise fallback to 2. Option 3 seems to force to have to handle each plugin manually, |
I agree on Option 3. I had to propose it since its a lighter version of Option 2 on the implementation but it's not viable for me. For Option 1 to work, we need to be even stricter about the order of the hooks. Maybe through the JSDoc /**
* @requires usePageSize
**/
const usePage = () => { ... } Or in TypeScript is we want to do a validation in the future for custom use hooks, but it may be overkill for now. |
I'm doing a first working draft of the state synchronous initialization Feature hook structureexport const useGridPage = (
apiRef: GridApiRef,
props: Pick<GridComponentProps, 'page' | 'onPageChange' | 'rowCount'>,
) => {
// 1. LOGGER
const logger = useGridLogger(apiRef, 'useGridPage');
// 2. STATE INITIALIZATION
// We initialize the state before 3. because if the hook is relying on selectors of its own state, it would crash
useGridStateInit(apiRef, (state) => ({
...state,
pagination: {
...state.pagination!,
page: 0,
pageCount: getPageCount(props.rowCount ?? 0, state.pagination!.pageSize!),
rowCount: props.rowCount ?? 0,
},
}));
// 3. STATE SELECTORS AND UPDATER
const [, setGridState, forceUpdate] = useGridState(apiRef);
const visibleRowCount = useGridSelector(apiRef, visibleGridRowCountSelector);
// 4. CONTROL STATE REGISTRATION
// We synchronously define the control state whereas before it was asynchronously defined
useGridRegisterControlState(apiRef, {
stateId: 'page',
propModel: props.page,
propOnChange: props.onPageChange,
stateSelector: (state) => state.pagination.page,
changeEvent: GridEvents.pageChange,
});
// 5. API METHODS
const setPage = React.useCallback<GridPageApi['setPage']>(
(page) => {
logger.debug(`Setting page to ${page}`);
setGridState((state) => ({
...state,
pagination: applyValidPage({
...state.pagination,
page,
}),
}));
forceUpdate();
},
[setGridState, forceUpdate, logger],
);
const pageApi: GridPageApi = {
setPage,
};
useGridApiMethod(apiRef, pageApi, 'GridPageApi');
// 6. UPDATE EFFECTS
// A major future evolution would be to limit as much as possible this pattern to avoid `useEffect` cascade.
React.useEffect(() => {
setGridState((state) => {
const rowCount = props.rowCount !== undefined ? props.rowCount : visibleRowCount;
const pageCount = getPageCount(rowCount, state.pagination.pageSize);
const page = props.page == null ? state.pagination.page : props.page;
return {
...state,
pagination: applyValidPage({
...state.pagination,
page,
rowCount,
pageCount,
}),
};
});
forceUpdate();
}, [setGridState, forceUpdate, visibleRowCount, props.rowCount, props.page, apiRef]);
// 7. EVENT CALLBACKS
const handlePageSizeChange = React.useCallback(
(pageSize: number) => {
setGridState((state) => {
const pageCount = getPageCount(state.pagination.rowCount, pageSize);
return {
...state,
pagination: applyValidPage({
...state.pagination,
pageCount,
page: state.pagination.page,
}),
};
});
forceUpdate();
},
[setGridState, forceUpdate],
);
useGridApiEventHandler(apiRef, GridEvents.pageSizeChange, handlePageSizeChange);
}; Points to clarifyFor the following points, the implementation I did seems non-optimal. Some state initialization are too complicated to be done at the start of their hookFor instance, the filter state is applied like this React.useEffect(() => {
// [...]
const oldFilterModel = apiRef.current.state.filter;
if (props.filterModel !== undefined && props.filterModel !== oldFilterModel) {
logger.debug('filterModel prop changed, applying filters');
setGridState((state) => ({
...state,
filter: props.filterModel,
}));
apiRef.current.applyFilters();
}
}, [apiRef, logger, props.filterModel, setGridState]); I added the So I did the following export const useGridFilter = (apiRef, props): void => {
const logger = useLogger('useGridFilter');
useGridStateInit(apiRef, (state) => ({
...state,
filter: props.filterModel ?? getInitialGridFilterState(),
visibleRows: {
visibleRowsLookup: {},
},
}));
// ... all the rest of the hook code
useFirstRender(() => apiRef.current.applyFilters());
} With export const useFirstRender = (callback: () => void) => {
const isFirstRender = React.useRef(true);
if (isFirstRender.current) {
isFirstRender.current = false;
callback();
}
}; Which means, if we have synchronous access to the state during the 1st render, Code duplication for state initializationHere is the code to init the useGridStateInit(apiRef, (state) => {
const hydratedColumns = hydrateColumnsType(
props.columns,
props.columnTypes,
apiRef.current.getLocaleText,
props.checkboxSelection,
);
const columns = upsertColumnsState(hydratedColumns);
let newColumns: GridColumns = columns.all.map((field) => columns.lookup[field]);
newColumns = hydrateColumnsWidth(newColumns, 0);
const columnState: GridColumnsState = {
all: newColumns.map((col) => col.field),
lookup: newColumns.reduce((acc, col) => {
acc[col.field] = col;
return acc;
}, {}),
};
return {
...state,
columns: columnState,
};
}); It's totally a duplicate of the The useEffect with
|
@oliviertassinari @m4theushw @DanailH when you have some time to take a look at the hook example above and the points to be improved about the typical structure of a feature hook. https://github.com/flaviendelangle/material-ui-x/pull/1/files The main issue that we must address before merging anything is Some components uses selectors of useGridReorder which is a pro-only hook. . |
Part of #2231
Goals
Why have each hook initialize its state ?
To better isolate XGrid only features (to reduce the size of the DataGrid bundle et don't put non-MIT code in it)
To allow a headless version of the Grid that would not import the unused features #1016
Why initialize it synchronously
The current state initialization is asynchronous.
On the first render, we have the default state.
Then each hook can update it to put values from the props.
For instance:
Which means on the 1st render, we have the default value even on hooks that are below this one.
Solutions
Approach 1
Pro
useDataGridComponent.ts
/useXGridComponent.ts
Con
Approach 2
Each hook can export a state initializer. All hooks are called in a single hook taking care of the state initialization and then running the hooks themselves.
Pro
useDataGridComponent.ts
/useXGridComponent.ts
for the state initialization when adding a new featureuseGridPlugins
Con
Approach 3
Each hook can export a state initializer. The
useXGridComponents
anduseDataGridComponents
are then responsible or initializing the state by manually calling each of those state initializer.Pro
Con
useDataGridComponent.ts
/useXGridComponent.ts
for the hook call AND for the state initialization (more error prone)Side effects
If we initialize the state synchronously, we will have to rework a bit the control state API.
It will not cause public behavior change.
The text was updated successfully, but these errors were encountered: