-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
Render prop API? #10476
Comments
Thanks! But does it make sense to limit |
Maybe my suggestion of using a generic for the
That way, the (BTW, in my initial bug report, I meant to refer to using a generic for |
For generic props to be useful you also need generic components, and unfortunately those aren't well supported currently; see microsoft/TypeScript#6395. I think this is fine, albeit not as type safe as one could hope: ContainerComponent?: React.ReactType<React.HTMLAttributes<any>>;
ContainerProps?: React.HTMLAttributes<any>; @oliviertassinari as a larger design note, the renderContainer?: (props: { className: string; children: ReactNode }) => ReactNode Here you can be very specific about the props ( |
@pelotom I have added the render prop API as an investigation area before releasing v1. Right now, the main use case for the |
The reason I suggested a render function instead of a component is that when you want to use an anonymous function to e.g. decorate with additional props, <Foo
ContainerComponent={({ className, children }) =>
<Bar className={className} someAdditionalFlag />
{children}
</Bar>
}
>
...
</Foo> you explicitly don't want that function to be treated as a component, because it will be a new "component" on every render, and therefore <ContainerComponent className={className}>{children}</ContainerComponent> then it's bad if ContainerComponent({ className, children }) I think it would be fine. |
@oliviertassinari TL;DR I'm basically just recapping this twitter thread which you were a part of 😄 |
@pelotom Right. On the other hand, you can create an intermediary variable to hold the reference to your custom component, then you can add extra properties. I think that it's really about how we can make the API the simplest to play with. const C = ({ className, children }) =>
<Bar className={className} someAdditionalFlag />
{children}
</Bar>
<Foo ContainerComponent={C}>
...
</Foo> |
That often requires you to do painful refactorings like turning this: const MyComponent = props => {
const x = ...;
const y = ...;
const z = ...;
return (
<Foo
ContainerComponent={({ className, children }) => (
<Bar className={className} x={x} y={y} z={z} />
{children}
</Bar>
)}
>
x={x}, y={y}, z={z}
</Foo>
)
} into this: class MyComponent {
ContainerComponent = ({ className, children }) => (
<Bar className={className} x={this.x} y={this.y} z={this.z} />
{children}
</Bar>
)
render() {
return (
<Foo ContainerComponent={this.ContainerComponent}>
x={this.x}, y={this.y}, z={this.z}
</Foo>
)
}
get x() {
return ...;
}
get y() {
return ...;
}
get z() {
return ...;
}
} Not to mention that most people won't even be aware they need to do that refactoring, and will be confused when their app is extremely inefficient.
Agreed, I think the simplest and friendliest thing for users would be to take a component prop but inline SFCs when rendering. |
If you just extract it to a variable in the render method, you haven't saved anything; it will still be a new component on every render. And if |
@pelotom I agree, I definitely see value in the render prop API. My concern is much more about the execution. Who do you think we should move forward?
It's a good point. |
I guess for the sake of backwards compatibility I would leave the props as they are, and just have special logic to render ContainerComponent?: React.ReactType<{ className: string; children: ReactNode }>;
ContainerProps?: { className: string; children: ReactNode; [k: string]: any }; Then TypeScript users wanting maximal type safety should avoid using |
This sounds like a good plan. You may have considered this already, but I just wanted to note that function components are allowed to have static properties like |
@pelotom We can take inspiration from the now removed recompose referential transparency optimization. But this is not exactly what the render prop is about. I will give more thoughts about it. At first sight, it sounds like a nice tradeoff. |
That's a good point. Probably want to check for at least
|
@oliviertassinari FYI - and I mention this because it wasn't mentioned in the release notes, although you might already be aware - in React 16, function components do have optimizations that class components don't have (I have an open PR to the React docs about this). That may be why recompose removed that feature. But if I understand correctly (and I may not), it would still be significantly more expensive to mount a function component than to just call a function directly. |
@mbrowne Thanks for linking the pull request. I can understand why recompose removed this behavior. In our case, it's not about performance. It's about getting a broken behavior when the component unmounts/mounts while the component is stateful. I think that the |
Yeah, I'm not either. It feels a little weird. Personally I would much prefer the simplicity of a render prop, that way there's no room for confusion. If we don't mind the breaking change, that's what I'd advocate. |
Render props are becoming increasingly common, and often preferred over other alternatives. I'm not saying they're always the best solution, but they're a good solution here. Of course backward compatibility also has to be considered, but I wonder how many people are already using the On the other hand, the concise syntax is nice, e.g. for something like this which is what I actually used the
...so a dual API (allowing the user to pass either a component or render function) could be nice for that reason... However, if it's decided that only supporting render props is the better API going forward, there are other approaches that could be taken in scenarios like this, e.g.:
Creating separate components would make sense at least for frequently reused |
(Note: I just edited my above comment; I didn't write the code I meant to write when I first sent it.) |
I had another thought...if you want to support both render props and passing components, you could actually have two separate props, e.g. Also, another alternative (to support a more concise syntax like the first version of my MenuItem Link example): use a helper function that takes any component class/function and returns a function that can be passed as a render prop. Then only the render prop API is needed. The helper function should probably just be a recommendation in the docs though, rather than part of material-ui. |
@oliviertassinari I just came across a library using a "hybrid approach between render prop and component injection": I did discover one issue with it, but it should be easily solved. However, I don't know if anyone is using this library "in the wild" yet. FYI I found the library via this article: https://blog.kentcdodds.com/answers-to-common-questions-about-render-props-a9f84bb12d5d |
@mbrowne Thanks for the links. I'm gonna work on this issue today and propose a plan of action. |
Let's benchmarkLet's start with a benchmark. Most of my job with Material-UI is to copy the good stuff while ignoring the other "bad" stuff.
(This table is by order of precedence)
The problems we are trying to solve:We want to provide a unified solution to the following problems:
Possible solutionsI believe the styles customization problem has been nailed by the introduction of the I'm proposing the following API. I have used the previous benchmark and the slot feature of vue as inspiration: import Fade from 'material-ui/Fade'
<Snackbar
// A regular property
open
// A property to change the root component
component="div"
// An object to override a whitelist of nested components.
// Why having a `component` and `components` property?
// Because `component` should be enough 80% of the time.
components={{
transition: Fade,
snackbarContent: props => <span {...props} />,
}}
// Similarly to the theme.props object feature,
// we can use it to inject properties on a whitelist of nested elements,
// the same whitelist than for the `components` property.
props={{
transition: {
appear: false,
},
snackbarContent: {
'e2e-test': 'snackbar-content',
},
}}
/> Regarding the Regarding the implementation. I think that we can minimize the overhead by using a whitelist of customization point and by building some helper functions. The whitelist will also enable us to automatically document it for each component. This is a breaking change. If we move forward in this direction, it means we can provide a systematic answer to the problems. We can remove the |
That's a though one. I'll try to write down my 2 cents about this. (1) IMHO the problem we want to solve and render props are orthogonal. Render props are more "higher level" (= more abstract). They will not let you change some specific part of the rendering process (e.g. change the transition from (2) Using a slot-like API is really really clever 🙇♂️ But Using more props than we already have, might complicate things. Especially for people that do not want to use the API. What about using a more component-oriented approach? For example using compound components like this: import Fade from 'material-ui/Fade'
<Snackbar
open
component="div"
>
<Snackbar.Transition>
{props => <Fade appear={false}/>}
</Snackbar.Transition>
<Snackbar.Content>
{props => <span {...prop} e2eTest="snackbar-content"/>}
</Snackbar.Content>
</Snackbar> This is just a quick shot about an alternative API. What I don't really like at the Using something like compound components could solve this. It is a (more) familiar API for React developer and most importantly it is not a MUI-specific API. Another nice side effect is that there are less props on the "host" component and less people will walk into the typo trap ( My only concern is performance. I am not sure if it is a good idea to have components share context here. I wonder if there are any widespread React implementations of the I would also suggest to think about the misuse of the new API. It would be nice to have it only work if you're using it the way it should be. PS: The slots also could be using render props, just to feed the hype! tl;dr I really like the idea of slots. I think that if @oliviertassinari estimate is correct and only 20% of the users need this feature, we should make sure that the API for the other 80% will not get more complex. |
I don't have a whole lot of time to dedicate to looking into this issue. But I just want to say that if you build the base component with a render prop + prop getters you can easily build other abstractions on top. In addition, there's less of an API to learn. I talk about this a bit in "Compose Render Props" So you could build a base component that uses a render prop + prop getters for any props that need to be applied to certain elements rendered, then build a few different components on top to cater to common use cases. This is one thing that has made downshift so flexible and simple. I'm going to have to unsubscribe from this issue now I'm afraid. I get way too many notifications. Feel free to ping me though if you want further clarification. |
Render props together with prop getters would make the API very powerful. E.g. import Fade from 'material-ui/Fade';
<Snackbar
open
component="div"
render={(children, getTransitionProps, getContentProps) => (
<Fade {...getTransitionProps()} appear={false}>
<span {...getContentProps()} e2eTest="snackbar-content">
{children}
</span>
</Fade>
)}
/>; But I wonder if this isn't to much abstraction. The downside of this approach is that people need to know the internal structure of the component. For example, if they only want to change the transition to That said, the API obviously would give the users the power to do whatever the fudge they want. I guess it depends on the use case what is better? |
There are missing props in the TypeScript type declaration file for ListItem, and also these types should ideally be generics.
Expected Behavior
It should be possible to use the
ContainerComponent
andContainerProps
props of theListItem
component from TypeScript. Also, I expected thatListItem
andMenuItem
would be generic types to account for the fact that any additional props are passed to the component specified by thecomponent
prop, but they're not. To be specific, in this file, the interface is currently defined like this:whereas I would have expected to see something like this:
Current Behavior
This TypeScript (.tsx) code does not compile:
Steps to Reproduce (for bugs)
Create the following test component in TypeScript:
Notes
The priority here is adding
ContainerComponent
andContainerProps
to the type declaration. Making it generic (e.g.MenuListProps<TContainerComponentProps = {}>
) is arguably a best practice and would be nice, but might not be so practically useful in this case. If it were a class component it might be more helpful, but as far as I can tell, TypeScript does not provide a way to alias generic functions that would be any more concise than my current solution of using a typecast:Your Environment
The text was updated successfully, but these errors were encountered: