Write Compound Components

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

Compound component gives more rendering control to the user. The functionality of the component stays intact while how it looks and the order of the children can be changed at will. We get this functionality by using the special React.Children.map function to map over the children given to our <Toggle/> component. We map over the children to pass the on state as a prop to its children. We move the visual pieces of the component out into function components and add them as static properties to <Toggle/>.

Instructor: [00:00] Now, let's say every time that I click on the toggle, I want it to tell me in text whether it's on or off. Let's say that I also want to be able to control where that text appears, but I don't want to change it right in the component.

[00:14] I'm going to be using this component in a lot of different places. Rather than just saying on and off, and then changing it in here, I want to be able to have that configurable outside of the component by the user.

[00:27] What I really want to do is, I want to be able to provide some children to this component, where I can say toggle.on, the button is on, and toggle.off, the button is off. Then I want to be able to position that button anywhere that I want, so I put the toggle dot button.

[00:48] Then I can reorder these however I like. The toggle component can be responsible for making sure that this renders when it's supposed to, this one does as well, and that this renders the switch. Let's go ahead and make that a reality.

[00:59] We're going to make a couple function components. Say toggle on, and this will take a prop on and children. Then it'll return if it's on the children, otherwise null. We'll do the same thing, except for toggled off.

[01:16] Instead of the children when it's on, it'll be the children when it's off. Finally, let's make the toggle button. That'll take on and toggle.

[01:25] We'll take the rest of the props, we'll return the switch with on is on, and onClick will be our toggle callback. Then we'll pass the rest of the props.

[01:40] To make them accessible off of the toggle class -- because we're going to put this in some sort of module, we want people to be able to access it right on the toggle class -- we'll say static on equals toggle on, and we'll do the same for off. Static button is the toggle button. Great, then we save that.

[02:04] Now, what do we render here? Right now, we're still rendering the switch, because we're ignoring the children of the toggle component. We're ignoring that prop, but we want to do something with those children.

[02:14] We're going to take those children, and we need to provide those children the props that they need to render properly. To do this, let's go ahead and make a children variable here. We'll say React.children.

[02:28] This has a whole bunch of utilities that we can use. One of those is map.

[02:32] This will allow us to map over all these children. We'll say list.props.children has the things that we want to map over. For each child, we're going to clone the child and provide additional props, like on, this.state.on, and toggle, this.toggle.

[02:51] Then we can get rid of that, and we'll return a div that has our children in it. I'll save that, and now we see the button is off, the button is on. I as the user of this component can move the button to the top, or I can even put the on text to be on top and the off text to be on bottom. I have total control.

[03:15] This is called compound components. We have one component here at the top level, then that component has some children components, and they share some implicit state. That implicit state is the on state. They can also share functionality like this button does.

[03:30] In review, what we're doing here in our render method is we're taking the map function from React.children -- which by the way, is not like array.map, but it's a special map function specifically for mapping over React children elements -- and we pass it this.props.children.

[03:46] For every child, we're going to clone that. That means it's going to take the elements that I give it, it's going to make a copy of it with the props that I provide here, so I'm passing the on and the toggle prop.

[03:59] That way, these components, when they're rendered, are able to use those on and toggle props to do their job.

Tim Anashev
Tim Anashev
~ 4 years ago

Why we need clone children? Can we pass just this.props.children instead of cloning elements?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Give it a try πŸ˜‰

You'll find that the components you render don't have the necessary props. We need to pass the props somehow, but we only have access to those props from within our toggle component. So our toggle component is responsible for passing those props in a way that implicit to users.

Jorge Alvarez
Jorge Alvarez
~ 4 years ago

I don't understand why you are passing the arguments of a function as an object, like in:

function toggleOn({on, children}) { ... }
James McAllister
James McAllister
~ 4 years ago

Thats just destructing the props.

same as

function toggleOn(props) { 
const {on, children} = props
...
}
` 
Pavel
Pavel
~ 4 years ago

Is it a good practice to pass 'toggle' method to all the children, since we use this method with one of them only? Or it's done just on a demonstration purpose?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Hi Pavel. The thing is that there's no reasonable way to know which child needs it and which doesn't. I mean, you could look at the type property of the child and that would allow you to know, but someone could make a custom component that expects to get the prop as well. No harm passing it to all of them πŸ‘Œ

Nick Renford
Nick Renford
~ 4 years ago

What is the purpose of adding

  static On = ToggleOn
  static Off = ToggleOff
  static Button = ToggleButton

to the Toggle component? Where are these properties referenced?

Paolo
Paolo
~ 4 years ago

Hi Kent, What about adding a component that is neither a Toggle.On, Toggle.Off, or Toggle.Button? Should there be some sort of error checking? When I think of compound components, I think of HTML's select and option tags. You can't add a div inside a select tag. Also, an option tag cannot be outside of a select tag. Is there ways to control that?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Sure! You could definitely add validation. See here: https://codesandbox.io/s/v835q7o193

You'll see I added an array of the valid types and then I check child.type against that array and log an error if the child's type is invalid. :)

Paolo
Paolo
~ 4 years ago

Thank you so much Kent! That was awesome for you taking the time to create a code sandbox example! Loving the tutorial!

0 plusX
0 plusX
~ 4 years ago

Can we use a Context API instead of cloning-and-assigning props? This will allow to pick values at any level of the tree. For example, if user adds containers as first level, and places Toggle.On inside these containers. Edit: Ahh... Its in the next lesson. Sorry...

Jan Borchers
Jan Borchers
~ 4 years ago

To follow up on Artyom's comment: I don't understand why we need to clone the children either. It works fine without:

https://codesandbox.io/s/y2p7or1509

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

I'm afraid that doesn't work. When you do: <Toggle.On /> that creates a react element which is an object, which is why you get this error in that codesandbox:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Jan Borchers
Jan Borchers
~ 4 years ago

argh, ignore me, I hadn't saved the editor, that's why it was still working πŸ˜‘πŸ˜‘πŸ˜‘

Edwin
Edwin
~ 4 years ago

I was trying to follow everything you did here in my environment which is using create-react-app. but I just don't understand how the Switch cab be looked like that. When I'd pasted your Switch code and CSS you provide, it just a checkbox and empty value button. am I missing something?

Johnny Zabala
Johnny Zabala
~ 4 years ago

Hi Edwin maybe you can try to download the codesanbox that Kent created and compare it with yours: https://codesandbox.io/s/v835q7o193

I downloaded, installed the dependencies and run it and it looks great.

Tony Brown
Tony Brown
~ 4 years ago

Very cool, I’m digging the the new api