Skip to content
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

Add the ability for consumers to render custom components #86

Merged
merged 7 commits into from
Jul 19, 2021

Conversation

james-woo
Copy link
Contributor

Allow consumers of RemoteRenderer to pass in their own custom component renderer or text renderer.

This will allow React Native to override the default behaviour of text rendering, since React Native requires that all Text objects to be wrapped in a ReactElement.

For web, we can remove the hack that gets the __type__ out of the component and move it back to web's side by overriding the component rendering.

}
return element ? cloneElement(element, {key: child.id}) : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to clone the element, or could the renderText/ renderComponent call be expected to do that? This component is rendered for every component in the tree so keeping it as lightweight as possible is definitely desirable

Copy link
Member

@henrytao-me henrytao-me May 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main issue is we are looping through the children which requires key. Is there a way to get around it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't renderComponent add the key?

renderText = defaultRenderText,
}: RemoteRendererProps) => {
const {children} = useAttached(receiver, receiver.attached.root)!;
const renderContextValue = useMemo(() => ({renderText, renderComponent}), [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a separate concept from the Controller, which already has a very related job of mapping remote component names => host component implementations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller is meant for rendering component type. We also has Text Component as component type which will be rendered using text type. This renderText is for text type.

Are you saying we should reserve a special Component in components list to render text type? This is what @vividviolet suggested before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller can be whatever we like it to be. Right now, it just gives the component implementation for a remote component, but I think it makes sense for createController to accept a second argument with the renderText stuff you've added here, and have those properties be exposed publicly such that we don't need a second piece of context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do it 👍

element = null;
break;
}
return element ? cloneElement(element, {key: child.id}) : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question regarding the need to clone the element

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about the key

@@ -1,41 +1,55 @@
import {memo} from 'react';
import {cloneElement, memo, ReactElement, useMemo} from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReactElement is a type, so should be a type-only import

receiver: RemoteReceiver;
controller: Controller;
renderComponent?: (props: RemoteComponentProps) => ReactElement;
renderText?: (props: RemoteTextProps) => ReactElement;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use method syntax instead of property syntax for these

@james-woo james-woo force-pushed the remote-renderer-custom-components branch from a2eb711 to 3c87912 Compare June 16, 2021 20:18
Copy link
Member

@henrytao-me henrytao-me left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great 👍

@james-woo james-woo requested a review from lemonmade June 18, 2021 13:44
Copy link
Member

@lemonmade lemonmade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good, just a few niggles/ suggestions left from me

packages/react/src/host/RemoteComponent.tsx Outdated Show resolved Hide resolved
packages/react/src/host/RemoteComponent.tsx Outdated Show resolved Hide resolved
packages/react/src/host/controller.tsx Outdated Show resolved Hide resolved
interface RendererFactory {
componentRenderer: (
defaultRenderer: Renderer['renderComponent'],
) => Renderer['renderComponent'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically this ⬆️

This is how the consumer can use the api

createController(components, {
  componentRenderer: (defaultRenderer) => (props) => {
    const {component} = props;
    switch(component.type) {
       case 'Card': 
         // Don't render Card
         return null;
       case 'Stack':
         // return custom Stack component
         break;
       default:
          return defaultRenderer(props);
    }
  }
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this with fresh eyes, this API feels a little too complex and verbose. I would prefer this:

createController(components, {
  renderComponent(component, {key, renderDefault}) {
    switch (component.type) {
      case 'Card': return null;
      case 'Stack': return <CustomStackComponent {...component.props} key={key} />;
      default: return renderDefault();
    }
  }
});

We get rid of the "arrow function returning arrow function" business and move some of the arguments around so that component/ text can be the first positional argument, which lets consumers destructure a little more confidently since they never need to pass the arguments they get back into another function. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we need to make the usage of key transparent? The consumer of this renderComponent should not know about why they need the key. If they forget to use key, it will complain because the part that needs it is in RemoteRenderer. Other than that, the approach looks good to me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way I can think to avoid needing to worry the consumer about the key would be to wrap each spot where we call these renderers in a <Fragment key={}></Fragment>. Not sure if that will work for your use case, and it's a bit annoying that we add an extra wrapping fragment around every component we create, but maybe it's OK?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be okay. Let's do it @james-woo. Action items:

  • Use Fragment for the key
  • Change signature to renderComponent(component, {renderDefault}). I think we still keep renderDefault in an object for future extensibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed Fragment as this wasn't compatible with passing the __type__ from web layer. Instead the consumer will have to remember to forward the key.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@james-woo james-woo requested a review from lemonmade June 21, 2021 13:59
@james-woo james-woo force-pushed the remote-renderer-custom-components branch from 7073262 to 7a50fe1 Compare June 22, 2021 15:45
Copy link
Member

@henrytao-me henrytao-me left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with James. LGTM 👍

packages/react/src/host/controller.tsx Outdated Show resolved Hide resolved
packages/react/src/host/controller.tsx Show resolved Hide resolved
packages/react/src/host/types.ts Outdated Show resolved Hide resolved
packages/react/src/host/controller.tsx Outdated Show resolved Hide resolved
@james-woo james-woo force-pushed the remote-renderer-custom-components branch from 6911017 to d688e85 Compare July 19, 2021 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants