Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 13, 2021 04:07 pm GMT

Compound components - React

What are compound components?

Compound components are just a set of components that belong to each other and work great together.
They are also super flexible and very scalable.

In this tutorial I will focus on a very simple card component example that hopefully explains itself and how easy the compound component pattern really is.

If you want more information on compound components, you can find a good amount of tutorials/videos out on the great internet, here are some of my favourites that made me start using the compound components pattern:

Kent C. Dodds - React Hooks: Compound Components

  • He uses function components with hooks and explains compound components well, but while he uses a great example for a use case, I think it's a little too hard to understand for beginners, because he uses useCallback and useMemo together with custom hooks and context (I also use context and custom hooks, but not using useCallback and useMemo I believe it is much easier to understand the concept of compound components).

Ryan Florence - Compound Components

  • This guy is funny and also explains compound components well. He uses class components which is just another (old?) way to create components and in my tutorial I focus on function components, just bear that in mind.

Card component as compound component

  1. The basics
  2. Creating a scope using context

  3. State management

  4. The power of compound components

The basics

Let's start with the example, which in the end is just a div that takes in the children prop:

function Card({children}){  return (    <div className="Card">      {children}    </div>  );}export default Card;

which is used like this:

<Card>  // Content goes here</Card>

At this point this is just a "normal" component, nothing special there.

Let's add a heading, say an h2:

function Card({children}){  ...}function Heading({children}){  return (    <h2 className="Card__heading">      {children}    </h2>  );}export Heading;export default Card;

Maybe you have already seen this way of defining components before (multiple components in the same file), or maybe you just know that this is possible. In theory this is actually almost all there is to compound components. It's that easy, because now you can do this:

<Card>  <Heading>My title</Heading></Card>

It is not so obvious that the Heading component "belongs" to the Card component, because you can just use the Heading component outside of Card:

<Heading>My title</Heading><Card>  // Oh no, I want my Heading to only be in here!</Card>

Let me show you a slightly different way of exporting the components:

function Card({children}){  ...}function Heading({children}){  ...}Card.Heading = Heading;export default Card;

Notice how I added the Heading component to the Card component as a property so the Heading now is a method of the Card object. This is because every component you make gets added to Reacts virtual DOM, which is just an object (a giant object), so if the Card component is just a property in the virtual DOM object, why not just add whatever you want to this Card property.

To illustrate it a little better, here is how you use it:

<Card>  <Card.Heading>My title</Card.Heading></Card>

I think this makes it more obvious that the Heading "belongs" to the Card component, but remember, it is just a component, so you can still use the Heading component outside the Card component:

<Card.Heading>My title</Card.Heading><Card>  // Oh no, I want my Heading to only be in here!</Card>

This is the very basics of compound components and you could stop here and say to yourself that you know how to create compound components, but there is so much more to compound components that make them super powerful and useful, especially in larger projects or for very complex components.

I'll go over most of them here:

Creating a scope using context

If we really want our child components to only work inside the Card component (what I call scope), we must do some extra work (obviously). Here we can take advantage of the context API (don't be scared if you don't fully understand the concept of context, just follow along and it should hopefully make sense. You can also read more about the context API if you want).

Let's start by creating the context by importing the createContext hook from React and create a variable called CardContext that uses this hook (you can call the variable whatever you like, but I think CardContext is a good, descriptive name):

import { createContext } from "react";var CardContext = createContext();function Card({children}){  ...}function Heading({children}){  ...  ...

We also need a provider for the context, but since we don't have any states or values we want to share via context we just use an empty object as the value in the value prop for the provider:

import { createContext } from "react";var CardContext = createContext();function Card({children}){  return (    <CardContext.Provider value={{}}>      <div className="Card">        {children}      </div>    </CardContext.Provider>  );}function Heading({children}){  ...  ...

The CardContext.Provider is, simply put, a container that holds any value value={// whatever you want} which is then available to all nested children.

To access the values we simply use the useContext hook in the child component that needs this access:

import { createContext, useContext } from "react";...function Heading({children}){  var context = useContext(CardContext);  return (    <h2 className="Card__heading">      {children}    </h2>  );}

Now the context variable holds whatever value we define in the value prop of the provider value={// whatever you want}, in our case this is just an empty object value={{}}.

The beauty of what we have created so far is that if we where to render <Card.Heading> outside <Card> (which is the provider), the context variable inside <Card.Heading> would be undefined, while if rendered inside, would contain the empty object {}.

Since this part is about scope and not about values available to child components through the use of context, let's create that scope by using the knowledge described above to make a condition check:

Condition check inside the child component
...function Heading({children}){  var context = useContext(CardContext);  if (!context) {    return (      <p className="Card__scopeError>        I want to be inside the Card component!      </p>    )  }  return (    <h2 className="Card__heading">      {children}    </h2>  );}

If we now try to render <Card.Heading> outside <Card>, a p-tag with our "error message" is rendered instead of our h2 which forces us to only use it inside <Card>. Great!

Although if we make a lot of child components we would have to copy/paste the context and the condition check into each and every one of them. That, I don't like very much. While it would work fine, the code would be very wet and not dry enough!

Combining condition check and context with a custom hook

All the code before the return statement inside <Card.Heading> can be boiled down to a single line using a custom hook which makes it a lot cleaner and easier to create new child components.

A custom hook is just a normal function with the benefit of having access to other hooks whether they are Reacts built in hooks like useState, useEffect, useRef and so on, or other custom hooks.
There is one important rule to creating custom hooks and that is to start your function names with the word "use":

function useObjectState(initialValue){  var [state, setState] = useState(initialValue);  return {state, setState};}

If you do this:

function objectState(initialValue){  var [state, setState] = useState(initialValue);  return {state, setState};}

you will get the following error:

React Hook "useState" is called in function "objectState" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter  react-hooks/rules-of-hooks

Okay then, let's create this custom hook (the hook is just copied from Kent C. Dodds' code. Link is at the top or click here):

import { createContext, useContext } from "react";...function useCardContext(){  var context = useContext(CardContext);  if (!context) {    throw new Error("Child components of Card cannot be rendered outside the Card component!");  }  return context;}function Card({children}){  ...

The sweet thing now is that every child component only have to use this custom hook, and the scope + context still works fine:

...function useCardContext(){  ...}function Heading({children}){  var context = useCardContext();  return (    <h2 className="Card__heading">      {children}    </h2>  );}...

That's it!

Now, we are still not using any value through the context, but trust me, it will work. Don't believe me? Okay then, let's do that next, shall we:

State management

Say we wanted a simple button in our card that when clicked, toggled the border color around our entire card and maybe the text color of our heading also toggles.

How would we do that?

Well let's create the button component first:

...function Heading({children}){  var context = useCardContext();  ...}function Button({children}){  var context = useCardContext();  return (    <button className="Card__button">      {children}    </button>  );}Card.Button = Button;...

and use it:

<Card>  <Card.Heading>My title</Card.Heading>  <Card.Button>Toggle</Card.Button></Card>

The button needs some state handling, so let's add that.
A rule of thumb:
Whenever we need to share state between our parent or child components, we should declare it at the parent level, in our case <Card>. And since we have already created our context, the sharing is just super easy:

import { createContext, useContext, useState } from "react";...function Card({children}){  var [toggled, setToggled] = useState(false);  return (    <CardContext.Provider value={{toggled, setToggled}}>      ...    </CardContext.Provider>  );}...

What we just did was to create a state with useState and added toggled and setToggled to the value prop of the provider (<CardContext.Provider value={{toggled, setToggled}}>. Did you notice how I "changed" the destructured array to an object with toggled and setToggled as properties?)

And then in <Card.Button> we grab setToggled and use it in our onClick event:

...function Button({children}){  var {setToggled} = useCardContext();  return (    <button      className="Card__button"      onClick={() => setToggled(prev => !prev)}    >      {children}    </button>  );}Card.Button = Button;...

I like the destructuring syntax, where we only "pull out" the things we need var {setToggled} = useCardContext();.
You could also use the context variable from before, but be aware of the syntax in the button onClick event onClick={() => context.setToggled(prev => !prev)}.

For the border to toggle in <Card> we just use the toggled state to toggle a CSS class on the div:

...function Card({children}){  var [toggled, setToggled] = useState(false);  return (    <CardContext.Provider value={{toggled, setToggled}}>      <div className={toggled ? "Card Card--highlight" : "Card"}>        {children}      </div>    </CardContext.Provider>  );}...

Last thing we need is to grab toggled from the context to make our heading also toggle color:

...function Heading({children}){  var {toggled} = useCardContext();  return (    <h2 className={      toggled        ? "Card__heading Card__heading--highlight"        : "Card__heading"}    >      {children}    </h2>  );}...

There you have it. You can now manage state inside your component and share it with the rest of your child components, without ever exposing it to the outside. As Ryan Florence says in his talk (link in the top or go to the video here):

There is state that lives inside of this system.
It's not application state. This isn't stuff that we wanna put over in Redux.
It's not component state because my component has its own state here.
This is it's own little system, it's own little world of components that has some state that we need to shuffle around.

So in compound component systems, you can create state that only lives inside this system, which in my opinion is very powerful.

The power of compound components

Compound components are super powerful, and if you read or have read this tutorial, you will see that I mention this a lot, and that's because they are both flexible and scalable, but also once you understand this pattern they are very easy to create, use and work with.

Flexibility

Did you notice that each of our child components (<Card.Heading> and <Card.Button>) only holds a single html (jsx) element? This is one of the things that makes the compound component pattern so very powerful, because now your <Card> component just became very flexible, for example you can do this if you want:

<Card>  // Who says the button should'nt be above the title?  // Well you do...! You decide where it should go.  <Card.Button>Toggle</Card.Button>  <Card.Heading>My title</Card.Heading></Card>

In contrast we could do this, which makes it look much simpler:

<Card title="My title" button={true} />

but who now decides which order the title and button is rendered in? Should we add a prop to place the button above?:

<Card title="My title" button={true} buttonAbove={true} />

Imagine having much more than two child components, how would you control the order then? A huge amount of props and sooo many if statements... No thanks!

Which leads me to the next powerful thing I want to talk about:

Scalability

How hard is it to add new features then?

Well, the short answer is: SUPER FREAKIN' EASY!

Let's do an example:

Say we want a flexible image. One where we can decide if it's a normal image that we just insert where we need it, or it is styled differently for example an avatar and maybe the option to insert an image as a background image.

Let's try:

...function Image({src, alt, type}){  useCardContext();  return (    <img      className={`Card__image${type        ? " Card__image--" + type        : ""}`}      src={src}      alt={alt}    />  );}Card.Image = Image;...

usage:

<Card>  <Card.Heading>My title</Card.Heading>  <Card.Image    src="/path/to/image.jpg"    alt="Our trip to the beach"  />  <Card.Button>Toggle</Card.Button></Card>

or:

<Card>  <Card.Image    src="/path/to/avatar-image.jpg"    alt="This is me"    type="avatar"  />  <Card.Heading>My title</Card.Heading>  <Card.Button>Toggle</Card.Button></Card>

You would off course need proper styling for Card__image--avatar and any other type you pass in.

So whenever you need a new feature, just add it in, it's that simple...

That's all folks. Hope you got some insights to the power of compound components and how easy it really is to use and create...


Original Link: https://dev.to/bqardi/compound-components-react-1ag8

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To