There are many ways of authoring components in React, and doing it right might not be that easy, especially when components get more complex. In this talk, you will learn how to build future-proof React components. We will cover two different approaches to building components - Composition and Configuration, to build the same component using both approaches and explore their advantages and disadvantages.
Composition vs Configuration: How to Build Flexible, Resilient and Future-proof Components
AI Generated Video Summary
Today's Talk discusses building flexible, resilient, and future-proof React components using composition and configuration approaches. The composition approach allows for flexibility without excessive conditional logic by using multiple components and passing props. The context API can be used for variant styling, allowing for appropriate styling and class specification. Adding variants and icons is made easy by consuming the variant context. The composition and configuration approaches can be combined for the best of both worlds.
1. Building Flexible and Resilient React Components
Today, I will discuss how to build flexible, resilient, and future-proof components in React. We will explore two approaches: composition and configuration. Building future-proof components can be challenging as the complexity grows. Adding features like headers, variants, and icons requires careful consideration of props and styles.
Hey, today I'm going to talk about how to build flexible, resilient, and future-proof components in React. I'm going to cover two different approaches to building components, composition and configuration.
But first, let me tell you a bit about myself. My name is Tomasz Findly and I'm a full-stack web and mobile developer with 10 years of programming experience. I'm a co-owner of Findly WebTech, as well as a mentor and consultant at CodeMentor.io platform. Besides that, I'm the author of React, the Road to Enterprise and Viewed the Road to Enterprise books. I also write articles for Telerik and the Road to Enterprise blogs.
Okay, that's enough about myself. Now, components. So, let's be honest. Building future-proof components is not easy. I mean, well, if you have a component like this, it's very simple. So for this example, I will use an alert component. So for example, if you would want to have an alert component that would display a basic alert message, well, we could do something like this, right? We could receive text entries as props, have some divs with styles, and then render what was passed, right? And here's how we could use it. Just use the alert component and pass the text message inside of it.
So, obviously, that's very simple, but the problem with building good components is that, as we need to add more functionality, the complexity just grows, right? The code is becoming much harder to maintain and extend. So what if we would want to add more features to this alert component? Let's say we just want to add a header as well. So we could receive a header as a prop, and if one was passed, we would display it, right? As shown here in the example. So, for instance, on the right side, we have one alert just with and the other one with both the alert header and the text message.
So what about variants? Well, I guess we could again add another prop, like variant, right? And then based on that prop, and whatever the variant was selected, we could add appropriate styles. So, for instance, we could support variants like success, danger, warning, or info, as you can see on the right side. And that's how we would use it. Just pass header and variant props and some text inside.
So what about adding, maybe, an icon? Well, again, another prop, like icon. If it was passed, and we found a supported icon, then we can render it. Simple, isn't it? And that's how we could use it. So, basically, by default, the icon could be displayed on the left. But what if we want to specify on which side we want it to be displayed? Like, maybe not on the left, but on the right? Well, guess what? Another prop! So, for instance, we could pass a prop like iconPosition, right? By default we could set it to left. And then, if, for instance, it was right, we could specify some classes, right, that would be added on the alert container. Note that I'm using Tailwind here for classes.
2. Using Composition for Flexibility
The configuration approach can become problematic when adding more functionality requires adding more props and conditional logic. It becomes harder to extend or overwrite the component's logic. On the other hand, composition offers a different approach. By using multiple components and passing props, we can achieve flexibility without excessive conditional logic. In the example, the alert component is composed of the alert wrapper, alert content, and alert body components, allowing for more control and easier extension.
And yeah, that's how we would use it, just more props. So, that was the configuration approach, right? Basically, whenever we just need to add more functionality, we add more props. But, well, that can be problematic at some point. Because the thing is, for every new variant and functionality, we need to add more and more props and conditional logic, right?
And sometimes it might just become much harder to overwrite the already defined logic inside of a component or even extend it. So, that's not really the best. The configuration approach makes it much harder. So, sometimes if a component can't be extended, we might need to build a new version of it. Well, as for pros, well, obviously a configuration approach, well, a component built with a configuration approach is quick and easy to use, right? Because you only need to know about what props are really available and what you need to provide. So, yeah. Basically different functionality and visual variants can be controlled via props and that's it. And another benefit of that is that it's much harder to diverge from the design system, right? Because the thing is you only can provide props and that's it. You can't do really anything else with it. So, well, this keeps the UI and behavior consistent.
But, yeah, like I mentioned the problem is that we can't easily extend the configuration build component or override it. So, what can we do? Well, I mean we could obviously provide maybe props like, let's say, a render icon, render header, you know, render body and so on and so on. But again, more props would be just more messy. So, there would be more conditional logic inside of the alert component. So, instead of trying to configure everything, how about we'll use a different approach, composition.
So, in this example, we have three components. First, the alert wrapper, then alert content, and alert body. And to the alert body, we pass the text message. I know three components just for the text message is a bit much, but stay with me. So, how it could look like? Basically, the alert component, obviously, it would receive some props. Then it would render a div with appropriate styles and children, right? So, in this case, the children would be the alert content and alert body. Then we have the alert content. As you see, it's very similar to the alert component. Because, again, it just receives the props. And has a div with some styles. And actually, well, the same will apply to alert body. I know there's a bit of repetition, and we have a few of the components already, but it's worth it at the end.
3. Using Context API for Variant Styling
In this example, we can use different components to build the alert functionality, including custom markup. To provide variants to components like alertHeader and alertBody, we can use the context API. The variant is passed as a prop to the variant context provider, which then provides it to all components in the component tree. This allows for appropriate styling and class specification for the alert component. The context factory creates the context and consumer for easy consumption in other components.
Just remember that this is a contract example. So, yeah, that's how we use it. Now, if we would want to add, let's say an alert header, well, we don't need to add a prop. We just add another component, like alert header, basically. So, all these components are just building blocks which we use to compose the functionality.
And what about, let's say, yeah, so first up again, our header in this case also is very similar to other components. It just receives the props, renders the diff with correct styles and then finally the children. Now next, what if we would want to have variance? Well, I guess maybe we could actually pass one prop to the alert component. Of course, we could also create a component for it. But for this example, this will do. So we'd pass a variant prop to the alert. But now the thing is that we can't compose the components right, to build the alert functionality. But the thing is that we can also use not only the building blocks, the components that we build specifically for the alert, but we can actually just use any custom markup we want. If we really wanted it, we could not use any of these building blocks, just the alert component.
So how do we provide a variant to all these other components like alertHeader and alertBody? Because they need to know what's the variant. Because they need to have appropriate styles. For instance, the header and body for the success variant have a dark green text, while the background for the alert is light. And also, we have the border on the left that is dark. So what we can do is we use the context API to provide the variant that was specified to all the components in the component tree. So in this case, we receive variant as a prop, and then we pass it to the variant context provider. Besides that, we also use it to specify appropriate classes for the alert component. Now, let's have a look how we can build this variant context provider. So I've used a little context factory helper, we'll get to it in a moment. Basically, it just returns a custom hook that will consume the context and the context itself. And as you can see, one line five here, the use variant context is exported so that it can be consumed in other components. And then in the variant context provider, we basically just render the provider and pass the variant to it. And that's it.
Next. So here we have the context factory, basically, it creates the context, creates the consumer and returns them. Now, alert header.
4. Using Variants and Icons
We update the alert header and body components to have access to the variant. We use the variant hook and consume the context to apply styles. Adding an icon is easy by using a pre-built component or custom markup. We use an icon config map to map icons to components. We consume the variant context to add appropriate styles to the icon. The composition approach is flexible and allows for easy extension of functionality. However, it requires composing building blocks and understanding their composition. The configuration approach only requires providing props.
Yes. So we need to now update the alert header and alert body component because they now also need access to the variant. So we import this variant hook, we consume the context and then we use it to apply appropriate styles. And we do the same in the alert body, again consume the context and apply the styles.
Okay. Next. So we have alert component with variants and icons, right. So if we wanted to add an icon, again, another building block, right, we can just use a component that was already built as part of the, let's say, you know, all the alert components and pass a specific icon. Or if you would want, we could even add some custom markup for it. Now, how would we use it? So first we can have an icon config map, where basically the icon is mapped to, let's say, sfg icons or whatever component you would want to use. Next, we use the icon prop to retrieve one of the icons that are supported. And then if we have one, then we render the markup for it. Besides that, we also consume the variant context so we can add appropriate styles to the icon, for example, correct colors. Like in our example, the success icon obviously will be green while warning icon will be orange. So as for positions, basically, we could move the alert icon component to the bottom and we could add some styles to it. For example, in this case, the alert icon, because it was moved to the bottom, it will be rendered after the alert content so it would be on the right side. And then we, for instance, can add some margin left auto with some right pad with some margin right value to have it positioned on the right side, as you can see here.
So that was the composition approach. So obviously the composition approach is extremely flexible, right, because you just basically use the components, which are building blocks, to compose the functionality. If you, for instance, need something more custom, you can just create a new markup, right, custom markup, or you can add more building blocks and that's that. So yeah, that's why the composition approach is really flexible. And it's not hard to, you know, extend functionality, or you don't even have to overwrite anything, right? Because you just compose everything. So, yeah, it is very easy to create different functionality and UI variants with composition approach. However, there are some cons to this approach, right? Because obviously you need to compose the building blocks yourself, right, to create this fully functioning component or feature. Because of that, you need to know how the building blocks work, what they do and how they should be composed. This isn't the case when using the configuration approach. Because with the configuration approach, you basically just need to know what props you're supposed to provide. And that's it. In the configuration approach, basically the component just takes care of everything, right, under the hood. Because you don't know...
5. Combining Composition and Configuration
The composition approach offers more flexibility but requires more knowledge of how to compose building blocks. The configuration approach is less flexible but simpler to use and helps maintain consistency. Both approaches can be combined to get the best of both worlds.
You might not know what's going on there. You just need to know the props and what values you need to pass. That's that. While with the composition approach, you do need to know what the building blocks are doing and how they are... how they should be used. And well, obviously, another con is that it does take more time and code, right, to create the same thing. Because, again, you need to compose the building blocks yourself.
And another disadvantage is that it's much easier to diverge from the design system and ship inconsistent UI and behavior. Obviously, because you basically can compose the building blocks, you know, however you want, right. So it would be easier to make mistakes and ship inconsistent UI just, basically, by providing wrong classes or ordering the building blocks in the wrong way. This isn't the case with the configuration approach. Because, again, you just provide props and that's it.
So, again, both approaches have their pros and cons, right. So the question is, when should we use which one? Well, technically, why not both? Right, because what we can do is first use the composition approach to basically build the, well, create the components that are the building blocks, right, and then we can use them to create a configure component, like here we have an example. Again, like we did in the configuration approach example, we receive a number of props, right, for the alert, then we have the baseline component, which basically accepts the variant and class name. Then we also have the alert icon. If the icon was passed, then it'll be rendered. And alert content, where if the header was passed, then alert component would be rendered. And if the text for children were passed, then alert body would be rendered. And yeah, that's how you can combine composition and configuration approaches to basically build components.
So most of the time, just you can use a configured component, but if you need more flexibility, right, then you have these building blocks available for you. So in summary, composition approach offers more flexibility, but it does require more knowledge of how to compose the building blocks, right? And how they work. On the other hand, configuration approach is less flexible, but it is simpler to use and makes it easier to stick to the design system. But yeah, we can combine both approaches to get the best of both worlds. So you can find code examples for this presentation in this GitHub repo.
What's more, if you'd like to learn more about advanced patterns, best practices, techniques for many crucial concepts such as local and global state management, scalable project architecture, performance optimization, managing APIs, testing, and much, much more, you should definitely check out React.io's Road to Enterprise. And if you'd like to get in touch, you can find me on Twitter, LinkedIn, CodeMentor, and the Road to Enterprise platform as well. Well, that's it for today.