-
Notifications
You must be signed in to change notification settings - Fork 58
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
Conversation
} | ||
return element ? cloneElement(element, {key: child.id}) : null; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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}), [ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
a2eb711
to
3c87912
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great 👍
There was a problem hiding this 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
interface RendererFactory { | ||
componentRenderer: ( | ||
defaultRenderer: Renderer['renderComponent'], | ||
) => Renderer['renderComponent']; |
There was a problem hiding this comment.
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);
}
}
});
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
7073262
to
7a50fe1
Compare
There was a problem hiding this 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 👍
6911017
to
d688e85
Compare
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.