Functional Attribute/Prop Node
nstepien opened this issue · comments
Consider the following: we're making a grid or table component with a lot of rows and cells that are heavy to compute, so we memo()
ize them. Let's take a look at a Row
component example:
import { memo, useContext } from 'react';
export default memo(Row);
function Row({ rowId, ...props }) {
const selectedRowIds = useContext(SelectionContext);
return (
<div role="row" aria-selected={selectedRowIds.has(rowId)}>
{/* render cells */}
</div>
);
}
We need to set the aria-selected
attribute accordingly when the row is selected.
But un/selecting 1 row will update the SelectionContext
, and will then trigger a re-render for all the Row
s.
An alternative to avoid breaking memoization too easily is too move the prop assignment to the parent component:
import { memo } from 'react';
export default memo(Row);
function Row({ ariaSelected, ...props }) {
function getCells() {
/* ... */
}
return (
<div role="row" aria-selected={ariaSelected}>
{getCells()}
</div>
);
}
This is okay if re-rendering the entire parent component is cheap enough.
What if there was a better way to do this? What if we could re-render the attribute only?
Something like this:
import { memo, useContext } from 'react';
export default memo(Row);
function Row({ rowId, ...props }) {
return (
<div role="row" aria-selected={<AriaSelected rowId={rowId} />}>
{/* render cells */}
</div>
);
}
function AriaSelected({ rowId }) {
return useContext(SelectionContext).has(rowId);
}
Now updating the SelectionContext
value only re-renders all the AriaSelected
/aria-selected
attributes, instead of all the Row
components, or the parent component, and none of the children.
Internally react-dom
can leverage document.createAttribute()
:
I don't know if the syntax should be the same since it's not an element anymore, and the key
& ref
props aren't special anymore. Maybe it would need the equivalent of React.createElement()
-> React.createAttribute()
.
Functional components and hooks are so good, and I think reusing the same patterns for attributes would be awesome.
I think such functional attributes should always be passed "rendered", they should be never be passed as is like components. So a component can never receive an attribute node, only its rendered value.
Unlike with components, I don't think there is really any value in passing attribute nodes.
For example
function MyComponent() {
return (
<ChildComponent
// passed as is, `ChildComponent` can do `React.cloneElement(props.saveButton, ...)`
saveButton={<Button {...props} />}
// the `Disabled` functional attribute is rendered after `MyComponent` is rendered, and before `ChildComponent` is rendered
disabled={<Disabled />}
/>
);
}
This would make introducing this feature a non-breaking change.
There wouldn't be any TS/Flow user typings changes to make either, an attribute that expects a boolean can also accept a functional attribute returning a boolean.
Hi, thanks for your suggestion. RFCs should be submitted as pull requests, not issues. I will close this issue but feel free to resubmit in the PR format.