reactjs / rfcs

RFCs for changes to React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 Rows.
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():
image

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.

commented

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.