When you're using prop collections, sometimes you can run into trouble with exposing implementation details of your prop collections. In this lesson, we'll see how we can abstract that away by simply creating a function called a prop getter that will compose the user's props with our prop collection.
Instructor: [00:00] In this usage example, we have some custom props that we're providing to the button. If I click on the button, that custom onClick handler is going to be called, but the toggle is not going to be called.
[00:12] The reason this is happening is because the toggler props that we're spreading across the button, that's intended to wire up our button to be a toggler, is providing an onClick handler. Then we override that onClick handler with our custom onClick here.
[00:26] We can see this behavior if we move the onClick above, and then we click. We're getting the toggle behavior back, but we're missing on our custom behavior that we want to apply.
[00:35] One solution to this is to in-line our function here. Then we can call onButtonClick, and then togglerprops.onClick. This will handle both use cases for us, but there are a couple things that I don't like about this.
[00:50] First of all, it requires this in-line function that we have to define just so we can get this button to behave like a toggler. We're already trying to do that by spreading the toggler props.
[01:01] The second thing I don't like is this exposes the implementation of the toggler props abstraction, which could lead to problems if we ever decide to change toggler props to use onkeypress rather than onClick, or if we decide we don't need onClick there at all.
[01:13] Any change to toggler props here would result in a breaking change for anybody using it in this way. What we really want is we want to have some sort of abstraction built into the toggle component that can compose the props that I want to apply the button with the props that the toggler needs to have applied to the button to wire it up properly.
[01:31] Instead of toggler props, we're going to expose a function called getTogglerProps. Here, we'll call this function, and we'll pass any props that we want to have applied.
[01:43] Now this is the API that we want to expose, a function which can accept an object of props, and composes that object of props with the props that are required for a button to be a toggler.
[01:55] Let's go ahead and implement this. We'll create a function that accepts props and returns those props, along with any of the props that we need for our toggler to function properly.
[02:07] Then we'll swap toggler props with this.getTogglerProps. Our switch is working fine, but we're not composing our click handler properly with this custom toggle button, so let's make that happen.
[02:19] We'll destructure the onClick from the props. Then here, we can provide a custom arrow function that calls onClick with all the arguments that an onClick handler is called with, and then calls this.toggle.
[02:35] Now if we run that, our toggle button is broken. That's because the toggle button doesn't actually provide an onClick, so let's say only call this if onClick is defined. Now we can toggle both of these, and everything is working great.
[02:52] Let's go ahead and refactor this, because I don't really like the way that this looks, and this is generally a pretty common use case for prop getters. I'm going to go up here, and we're going to create a function called callAll. That's going to accept any number of functions, and that'll return a function that accepts any number of arguments.
[03:11] Then we'll say functions for each. For each function, if that function exists, then we'll call it with the arguments.
[03:19] Then we can use callAll right here. We'll call the onClick, and this.toggle. That's functionally equivalent to what we had before, just looks a little bit cleaner. Now everything is working exactly as it is intended.
[03:36] In review, the problem we're trying to solve is there's a common use case for rendering in the toggle component, and that is to render a toggler button. Any of the users of this toggle component will probably be rendering a toggler.
[03:48] We want to provide a convenience method for them so they don't have to worry themselves about implementation details. They can simply apply the get toggler props.
[03:57] The problem with toggler props is we couldn't compose things together without exposing implementation details. The user of the toggler props shouldn't have to know that we're using an onClick, and that we're providing an aria-pressed.
[04:08] They should just be able to spread the props that we provide them, and everything should be wired up correctly. Instead of a toggler props object, we expose this get toggler props function, which they pass any of their props to. Then those props will be returned by the get toggler props component, composing the behavior that is necessary.
[04:28] Another situation where this could be useful is if we wanted to apply a class name here. We could accept a class name, and then we'd have our class name be the combination of their class name with our custom class name. We could do this with the style prop, and any other prop where it makes sense to compose behavior.
[04:50] This API also allows people to easily override any of this behavior. If they want to override the aria-pressed, then they can provide an aria-pressed to be null.
[05:01] If they want to completely override the built-in onClick behavior, then they could actually provide an onClick on button click. Now it's not wired up properly, but maybe that's exactly what they're looking for. This API gives them that flexibility.
Hi Erez, That's a good point! I actually had that functionality in downshift since the initial release. It is awesome! The problem is that if you want to prevent downshift's behavior but not the browser's default behavior you run into issues.
I don't demonstrate how to accomplish this behavior in the course because it's not super common, but maybe I'll add it to the course eventually. By the way, where we landed in 2.0.0 is setting event.nativeEvent.preventDownshiftDefault = true
due to issues with React and warnings and things. That seems to work pretty well.
How do you avoid unwanted props to be spread across components that doesn't need them?
For example, <Switch />
doesn't need this prop ('aria-pressed') to function. To explicitly set 'aria-pressed' to null requires knowledge of the implementation detail of <Toggle />
so it is still taking the abstraction way.
Furthermore, inside the return object of getTogglerProps, we need to know in advance what prop is going to be defined both within <Toggle />
and the user's custom usage (and also to extract that props in the input arguments).
For example, if the one who wrote the Toggle component wasn't aware that someone in the future is going to use the onClick prop on the button then how can s/he know in advance that onClick should be extracted and treat specially inside getTogglerProps?
So this pattern seeks to solve a problem that we end up not solving. Am I seeing it wrong?
Phyllis
The idea of prop getters is that you don't know/care what props are necessary for an element to assume the role of a toggler (in this case) or an autocomplete input (like in the case of downshift's getInputProps
). If you notice there are props that you don't want applied for some reason then you can either explicitly opt-out via an override as you suggested, or perhaps what you're rendering isn't the typical toggler and you shouldn't use the prop getter. There's nothing stopping you from not using the prop getters and applying your own props as all the helpers are still available for you to toggle the state.
So this pattern seeks to solve a problem that we end up not solving
If you look at everyone using downshift's prop-getters then you'll see that it definitely solves a problem. The alternative to prop-getters is everyone knowing that they need to supply their input with all of these props. We're definitely solving a problem here 😉
Massive thanks for teaching us about getProps. I'm in the process of adopting my old UI toolbox to use it and not looking back - the simplicity and flexibility gain (and therefore reuse potential) is massive compared to the mix and match of other patterns I've been using in the past.
A quick question - is there any reason ever to use compound component pattern in favor of getProps? I can only think of cases when you want to make a component very easy to just "plug and play" out of the box and don't care about granting the user flexibility much if at all (I used MaterialUI and React Select in the past and they had the same interface), and forcing the user to learn your API (and who has time for that?) if he wants to dig deeper. However, it's just as easy to set up default render functions for cases when user has no time to mess with setting up the render props chain.
I tried to make compound components work for a long time and in many different combinations and they just seem so clumsy especially when mixed with other patterns.
re: what Wix is saying, instead of callAll I just use the following onClick inside the object returned from getFooProps:
onClick(e, ...args) => {
e.stopPropagation // and /or e.preventDefault etc.
anyOtherFunction(anyOtherArguments) // can be e, args, or anything else - e.g. when rendering a user's custom list this could be a list value restructured from getFooProps
onClick && onClick(e, ...args)
}
is there any reason ever to use compound component pattern in favor of getProps
Each of these patterns caters to different use cases/preferences. Render props is the most primitive of the render flexibility patterns and you can build other component APIs out of that pattern, so I'd generally build the render props pattern and then build compound components on top of it for a slightly different API that some people seem to prefer. Here's a real world example of doing that with downshift: https://codesandbox.io/s/github/kentcdodds/downshift-examples/tree/master/?module=%2Fsrc%2Fother-examples%2Fhoc%2Findex.js&moduleview=1
Good luck!
Oh, and to address onClick && onClick(e, ...args)
, that's fine, but if you have several of these, it's nice to have the callAll
abstraction. See https://github.com/paypal/downshift/blob/master/src/downshift.js
@ 02:51. If onClick is not defined we will get the cannot destructure onClick from undefined error. So I fail to see what the onClick && onClick(...args)
is guarding against. Unless, of course, we pass the undefined onClick.
I think that when composing event handlers, it is useful to support the event.preventDefault() mechanism. So in
callAll()
method you could check event.defaultPrevented before calling each function, and stop of default is prevented.This let's you expose an API similar to the native one. Mostly makes sense when you are implementing a simple component which wraps a single native element.
Its true that in this example you could prevent the togller's onClick from being called, by supplying the onClick after the spread of {...togglerProps}, but if some consumer of this component is passing onClick to a component which uses the Toggler, then that would not be possible.
Erez erezm@wix.com