-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Currently all renderers (grid cells, dashboard widgets, ...) are treated as actual components rather than just functions that render children. That means that the React virtual DOM will contain an instance of that component, rather than just the children that the function returns. That is problematic with how we advertise using renderers in the docs:
<Grid items={items.value}>
<GridColumn header="Employee">
{({ item }) => <span class="foo">{item.fullName}</span>}
</GridColumn>
...
</Grid>
- With this example, it creates a new function on each render cycle
- Since we treat those as a component, it means it actually creates a new component type each render cycle
- So the virtual DOM contains a different type of component on each render cycle
- That results in React fully replacing the actual DOM, and any state is lost (focus, text selection, entered text)
From just looking at the code, this is also easy to miss:
- It's not obvious that the function is now an actual component and that it changes how React updates the DOM
- A render function is a common concept in the React ecosystem and the example above looks like one. However a render function is typically just called directly instead of creating a component instance from it
Option 1: Treat renderers as just render functions
We should consider treating renderers as as just render functions in their traditional sense. So if the renderer is a function, we just call that function instead of creating a component instance with React.createElement
.
By doing that, the virtual DOM will always contain the same type of component. For example, if a function returns a button, then the virtual DOM will always just contain a button. That allows React to update the current DOM instead of replacing it completely.
We need to check if this can be done without breaking changes:
- We currently allow passing any type of React component, included class-based components, which can not be called as a function. As a workaround, we might be able to render as component as we do now, but then discard it and instead add its children to the virtual DOM.
- Long term, we should only allow passing functions.
- Need to check if there is some way to use refs with renderers that could break.
Option 2: Improve docs to clarify that renderers are components
If we actually want renderers to be components, we could update our docs make sure that users are guided to never recreate renderers on each render cycle. For example by memoizing the component like so:
const employeeRenderer = useCallback(
({ item }) => <span class="foo">{item.fullName}</span>,
[]
);
<Grid items={items.value}>
<GridColumn header="Employee">
{employeeRenderer}
</GridColumn>
...
</Grid>
However, using components make trivial use-cases more complex. For example:
const [selectedItems, setSelectedItems] = useState<Person[]>();
const selectionRenderer = useCallback(
({ item }) => (selectedItems.includes(item) ? <SelectedIcon> : <UnselectedIcon>),
[]
);
<Grid items={items.value}>
<GridColumn>
{selectionRenderer}
</GridColumn>
...
</Grid>
Here a renderer uses the selection state from the component that it's used in. The problem is that it doesn't actually work, because the renderer component will always reference the initial selection state. To fix this, a new renderer component would have to be created each time the selection changes, which brings us back to the initial problem. There are ways to solve this using React contexts or signals, but still:
- The complexity increases
- There is potential for subtle bugs