[data grid] Recommended way to use `apiRef` in render phase?
The problem in depth
In react 19.2 the eslint-plugin-react-hooks v7.0.0+ has been made stricter, in particular with two rules:
https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect https://react.dev/reference/eslint-plugin-react-hooks/lints/refs#invalid
I'm curious what usage patterns mui-x recommends when using the grid api as we are struggling with a lot of these in our current codebase. The react docs don't offer like for like valid vs invalid examples.
For example, we have some code based on https://github.com/mui/mui-x/blob/249684a042364ebeb886c61673b42d98013687b4/packages/x-data-grid-pro/src/components/GridDataSourceTreeDataGroupingCell.tsx#L88 and would like to know if you have any recommended ways around this? We can't put the apiRef in a useEffect and then set state with the label. Similarly we can't access the ref in the render phase.
Thanks
Your environment
`npx @mui/envinfo`
Don't forget to mention which browser you used.
Output from `npx @mui/envinfo` goes here.
Search keywords: ref apiRef
Order ID: 45466
Hey @mbiggs-gresham before diving deeper into this topic I would like to know what you are trying to do with the apiRef. And by render phase do you mean any render phase or just the initial render?
Hi, it's kind of a generic problem however to begin with how would you fix your own GridDataSourceTreeDataGroupingCell as that will fail the ref rule on line 88? You are only allowed to use a ref within a useEffect hook or a handler.
And it will fail the rule on any render, not just the initial. As soon as a ref is spotted within the components body it will fail the rule (with the exception of the ref being initialised).
Got it. IMHO the rule is a bit too strict, since it does not consider refs that are safe to use (like in our case where we make sure that its initialized before we use it). and for the suggested solution for this: I would go with the proposed way from the docs. Since this is so restrictive there does not seem to be a good way of doing it otherwise.
const [ariaLabel, setAriaLabel] = React.useState<null | string>(null);
React.useLayoutEffect(() => {
setAriaLabel(
rowNode.childrenExpanded
? apiRef.current.getLocaleText('treeDataCollapse')
: apiRef.current.getLocaleText('treeDataExpand')
)
}, [rowNode.childrenExpanded, apiRef.current.getLocaleText, apiRef]);
It does not state if it is allowed to make us of useMemo or something, right?
The rules have caused us quite a few issues, and I do kind of agree they are too strict, however they're coming directly from react itself, and failing to fix them can result in the component not being optimised by the react compiler.
In your solution code, the apiRef still violates the ref rule because it is used in the dependency array. Further more, setting state within a useEffect or useLayoutEffect violates the other rule. This is exactly the problem we have faced, in that the common solutions we would normally come up with aren't valid anymore.
You can use useMemo or useCallback as those execute within the render, whereas the useEffect runs after the render and therefore setting state triggers a 2nd render, however if you try to use the ref within the dependency array of a useMemo or useCallback it will not allow it.
In your solution code, the
apiRefstill violates the ref rule because it is used in the dependency array. Further more, setting state within auseEffectoruseLayoutEffectviolates the other rule. This is exactly the problem we have faced, in that the common solutions we would normally come up with aren't valid anymore.
well, that's true. Using it in the dep-array will error out. Honestly with this new rule refs seem to be much less useful. How are you even supposed to use it when you cannot use it in the render body, nor in a dependency array (in case something changes with it - side note: leaving a ref out will violate the exhaustive-deps rule 🤔)
You can use
useMemooruseCallbackas those execute within the render, whereas theuseEffectruns after the render and therefore setting state triggers a 2nd render, however if you try to use the ref within the dependency array of auseMemooruseCallbackit will not allow it.
I struggle to find a good enough pattern to use it here. @romgrk or @MBilalShafi ideas?
Yes it's horrible isn't it. The reason I posted here is that it makes using the GridApi really difficult in a lot of situations like we could before and was hoping you had a workaround or suggestion.
Not really, since we are storing it in a ref to ensure stability across renders. A possible solution to this would be to move away from refing it and instead create a context or similar to hold the api object. But this would mean a massive refactoring and is likely not going to happen blindly. Let's see what the others come up with first.
Considering the dependency array: It should be safe to use the stable object (not .current) in it, so this should be fine:
React.useEffect(() => {
const api = apiRef.current;
if (!api) return;
return api.subscribeEvent('rowClick', (params) => {
api.setRowSelectionModel({ ids: new Set([params.id]), type: 'include' });
});
}, [apiRef]); // depend on the ref object itself, not .current
From what I understand, this falls under the "✅ Lazy initialization of ref value" example in React docs, and is a valid use-case. The issue is the eslint rule, not the code; the code is safe. The React eslint rules are implemented with a very naive algorithm and trigger many false positives. You should disable the rule in those cases, not try to make up for it at runtime. This note is relevant, you can rename the variable to avoid the rule.