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.
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.
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.
First, create a Form component. This component acts as a container where subcomponents (TextInput, Checkbox, Select, etc.) will live.
1import React, { ReactNode } from "react";23 // Define type for form component props4 interface FormProps {5 children: ReactNode;6 }78 const Form: React.FC<FormProps> = ({ children }) => {9 return <form>{children}</form>;10 };1112 export default Form;
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";23 // Define type for form component props4 interface FormProps {5 children: ReactNode;6 }78 const Form: React.FC<FormProps> = ({ children }) => {9 return <form>{children}</form>;10 };1112 export default Form;
From
Component with Childrens1import React, { ReactNode } from "react";2 import { TextInput, TextInputProps } from "./text-input";3 import { Checkbox, CheckboxProps } from "./checkbox";4 import { Select, SelectProps } from "./select";56 // Define type for form component props7 interface FormProps {8 children: ReactNode;9 }1011 interface FormComposition {12 TextInput: React.FC<TextInputProps>;13 Checkbox: React.FC<CheckboxProps>;14 Select: React.FC<SelectProps>;15 }1617 const Form: React.FC<FormProps> & FormComposition = ({ children }) => {18 return <form>{children}</form>;19 };2021 Form.TextInput = TextInput22 Form.Checkbox = Checkbox23 Form.Select = Select2425 export default Form;
TextInput
, Checkbox
, and Select
components are imported along with their respective prop types.Form
component. This approach, known as the composite component pattern, allows developers to access subcomponents using dot notation (e.g., Form.TextInput
).Form.TextInput
, Form.Checkbox
, and Form.Select
as valid properties with their respective prop types, we define an interface called FormComposition, which lists all the components along with their typesApp
Component to Use Form
SubcomponentsNow, 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 directory34 type FormState = {5 name: string;6 subscribe: boolean;7 gender: string;8 };910 const App = () => {11 const [formState, setFormState] = useState<FormState>({12 name: '',13 subscribe: false,14 gender: '',15 });1617 const handleInputChange = (field: keyof FormState, value: any) => {18 setFormState((prev) => ({ ...prev, [field]: value }));19 };2021 return (2223 <div>24 <h1>Dynamic Form</h1>25 <Form>26 <Form.TextInput27 label="Name"28 value={formState.name}29 onChange={(value) => handleInputChange("name", value)}30 />31 <Form.Checkbox32 label="Subscribe to newsletter"33 checked={formState.subscribe}34 onChange={(checked) => handleInputChange("subscribe", checked)}35 />36 <Form.Select37 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 ); };4647 export default App;
Form
), we can keep our code organized and reusable.Form.TextInput
, Form.Checkbox
, and Form.Select
usage is intuitive and resembles object-oriented structures.Form.RadioButton
) while keeping the API consistent.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.