Nov 10, 2024

Building Reusable Components with the Composite Component Pattern in React and TypeScript

Introduction

In this post, we'll explore the Composite Component pattern, one of the most powerful design patterns for building reusable UI components in React. This pattern helps to combine simple components into more complex ones, promoting modularity and code reusability.

We'll walk through a real-life example of building a Form component where different input fields (like Form.TextInput, Form.Checkbox, and Form.Dropdown) can be composed together dynamically using TypeScript. By the end of this post, you'll understand how to effectively use the Composite Component pattern to create flexible, maintainable UIs.

What is the Composite Component Pattern?

The Composite Component Pattern allows you to group smaller, reusable components under a single parent component.

Imagine a toolbox: instead of having tools scattered everywhere, you organize them into a single box. Similarly, components like TextInput, Checkbox, and Dropdown can be grouped into a Form component namespace.

Real-Life Example: Dynamic Form Builder

Let’s imagine you're building a form that requires several different types of inputs, such as text fields, checkboxes, and dropdowns. Instead of creating a separate form for each use case, you can create a composite form where various input components can be added dynamically using Form.Component syntax in TypeScript.

Step 1: Create the Parent Form Component

First, create a Form component. This component acts as a container where subcomponents (TextInput, Checkbox, Select, etc.) will live.

1import React, { ReactNode } from "react";
2
3 // Define type for form component props
4 interface FormProps {
5 children: ReactNode;
6 }
7
8 const Form: React.FC<FormProps> = ({ children }) => {
9 return <form>{children}</form>;
10 };
11
12 export default Form;
Step 2: Create Few Subcomponent

Now let’s add TextInput, Checkbox and Select as child components which just takes few props and renders some ui element.

1import React, { ReactNode } from "react";
2
3 // Define type for form component props
4 interface FormProps {
5 children: ReactNode;
6 }
7
8 const Form: React.FC<FormProps> = ({ children }) => {
9 return <form>{children}</form>;
10 };
11
12 export default Form;
Step 3: Connect the From Component with Childrens
1import React, { ReactNode } from "react";
2 import { TextInput, TextInputProps } from "./text-input";
3 import { Checkbox, CheckboxProps } from "./checkbox";
4 import { Select, SelectProps } from "./select";
5
6 // Define type for form component props
7 interface FormProps {
8 children: ReactNode;
9 }
10
11 interface FormComposition {
12 TextInput: React.FC<TextInputProps>;
13 Checkbox: React.FC<CheckboxProps>;
14 Select: React.FC<SelectProps>;
15 }
16
17 const Form: React.FC<FormProps> & FormComposition = ({ children }) => {
18 return <form>{children}</form>;
19 };
20
21 Form.TextInput = TextInput
22 Form.Checkbox = Checkbox
23 Form.Select = Select
24
25 export default Form;
Step 4: Create the App Component to Use Form Subcomponents

Now, let's use the Form component and its subcomponents like Form.TextInput, Form.Checkbox, and Form.Select to build a dynamic form. We'll handle form state using TypeScript types to ensure strong typing.

1import React, { useState } from 'react';
2 import Form from './Form'; // Assuming Form is in the same directory
3
4 type FormState = {
5 name: string;
6 subscribe: boolean;
7 gender: string;
8 };
9
10 const App = () => {
11 const [formState, setFormState] = useState<FormState>({
12 name: '',
13 subscribe: false,
14 gender: '',
15 });
16
17 const handleInputChange = (field: keyof FormState, value: any) => {
18 setFormState((prev) => ({ ...prev, [field]: value }));
19 };
20
21 return (
22
23 <div>
24 <h1>Dynamic Form</h1>
25 <Form>
26 <Form.TextInput
27 label="Name"
28 value={formState.name}
29 onChange={(value) => handleInputChange("name", value)}
30 />
31 <Form.Checkbox
32 label="Subscribe to newsletter"
33 checked={formState.subscribe}
34 onChange={(checked) => handleInputChange("subscribe", checked)}
35 />
36 <Form.Select
37 label="Gender"
38 options={["Male", "Female", "Other"]}
39 value={formState.gender}
40 onChange={(value) => handleInputChange("gender", value)}
41 />
42 </Form>
43 <pre>{JSON.stringify(formState, null, 2)}</pre>
44 </div>
45 ); };
46
47 export default App;
Benefits of the Composite Component Pattern
  1. Modularity - By grouping related components under a single namespace (Form), we can keep our code organized and reusable.
  2. Readability - The Form.TextInput, Form.Checkbox, and Form.Select usage is intuitive and resembles object-oriented structures.
  3. Strong Typing with TypeScript - With TypeScript, you can ensure that all component props are type-checked, reducing bugs and improving developer experience.
  4. Extensibility - It becomes easy to add more components (like Form.RadioButton) while keeping the API consistent.
Conclusion

The Composite Component pattern is a simple yet powerful way to build dynamic, flexible UIs in React. By decomposing complex interfaces into smaller, reusable components under a common parent (Form in our case), we can build maintainable and scalable applications. With TypeScript, we also gain the benefits of strong typing, making our components safer and easier to work with. I hope this real-life example of a dynamic form builder inspires you to use this pattern in your own projects.

contact

padhysoumya98@gmail.com

Soumya

2025