
ReactJS
Advanced props patterns unlock component architectures that are flexible, maintainable, and scalable. Compound components, render props, higher-order components, and composition strategies represent the highest levels of component design.
Compound components use context and children props to build related components that manage shared state. Unlike prop drilling where data travels through multiple levels, compound components coordinate at the same level.
This pattern is powerful for building complex UI systems like forms, tabs, dropdowns, and dialogs where multiple sub-components need to share state without the parent coordinating everything.
import { createContext, useContext, useState, ReactNode } from "react";
// Create context for compound component state
interface TabsContextType {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextType | undefined>(undefined);
interface TabsProps {
defaultActive: string;
children: ReactNode;
}
// Parent compound component
function Tabs({ defaultActive, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultActive);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext.Provider>
);
}
interface TabTriggerProps {
id: string;
children: ReactNode;
}
// Sub-component that reads and modifies context
function TabTrigger({ id, children }: TabTriggerProps) {
const context = useContext(TabsContext);
if (!context) throw new Error("TabTrigger must be used inside Tabs");
return (
<button
role="tab"
aria-selected={context.activeTab === id}
onClick={() => context.setActiveTab(id)}
style={{
fontWeight: context.activeTab === id ? "bold" : "normal",
borderBottom: context.activeTab === id ? "2px solid blue" : "none",
}}
>
{children}
</button>
);
}
interface TabContentProps {
id: string;
children: ReactNode;
}
// Sub-component that conditionally renders based on context
function TabContent({ id, children }: TabContentProps) {
const context = useContext(TabsContext);
if (!context) throw new Error("TabContent must be used inside Tabs");
return context.activeTab === id ? <div>{children}</div> : null;
}
// Usage: no prop drilling, clean API
function MyTabs() {
return (
<Tabs defaultActive="home">
<div>
<TabTrigger id="home">Home</TabTrigger>
<TabTrigger id="profile">Profile</TabTrigger>
<TabTrigger id="settings">Settings</TabTrigger>
</div>
<TabContent id="home">Home content</TabContent>
<TabContent id="profile">Profile content</TabContent>
<TabContent id="settings">Settings content</TabContent>
</Tabs>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Compound form pattern
interface FormContextType {
values: Record<string, any>;
errors: Record<string, string>;
setFieldValue: (name: string, value: any) => void;
setFieldError: (name: string, error: string) => void;
}
const FormContext = createContext<FormContextType | undefined>(undefined);
interface FormProps {
initialValues: Record<string, any>;
onSubmit: (values: Record<string, any>) => Promise<void>;
children: ReactNode;
}
function Form({ initialValues, onSubmit, children }: FormProps) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await onSubmit(values);
} catch (error: any) {
setErrors({ submit: error.message });
}
};
return (
<FormContext.Provider
value={{
values,
errors,
setFieldValue: (name, value) =>
setValues((v) => ({ ...v, [name]: value })),
setFieldError: (name, error) =>
setErrors((e) => ({ ...e, [name]: error })),
}}
>
<form onSubmit={handleSubmit}>{children}</form>
</FormContext.Provider>
);
}
interface FormFieldProps {
name: string;
label: string;
type?: string;
}
function FormField({ name, label, type = "text" }: FormFieldProps) {
const context = useContext(FormContext);
if (!context) throw new Error("FormField must be in Form");
return (
<div>
<label>{label}</label>
<input
type={type}
name={name}
value={context.values[name]}
onChange={(e) => context.setFieldValue(name, e.target.value)}
/>
{context.errors[name] && (
<span style={{ color: "red" }}>{context.errors[name]}</span>
)}
</div>
);
}
// Usage: declarative, composable form structure
<Form
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
await api.login(values);
}}
>
<FormField name="email" label="Email" type="email" />
<FormField name="password" label="Password" type="password" />
<button type="submit">Login</button>
</Form></details>
The render props pattern uses a function as a child or prop to give parent components fine control over rendering logic while child components manage state. This pattern enables powerful abstractions for logic reuse.
// Simple example: Data fetching render prop
interface DataFetcher<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
interface FetchProps<T> {
url: string;
children: (state: DataFetcher<T>) => ReactNode;
}
function Fetch<T>({ url, children }: FetchProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const load = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
load();
}, [url]);
return children({ data, loading, error });
}
interface User {
id: string;
name: string;
}
// Usage: parent controls rendering while child manages fetching
<Fetch<User[]> url="/api/users">
{({ data, loading, error }) =>
loading ? (
<div>Loading...</div>
) : error ? (
<div>Error: {error.message}</div>
) : (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
</Fetch><details>
<summary>📚 More Examples</summary>
// Example 2: Mouse position tracker with render props
interface MouseTrackerProps {
children: (position: { x: number; y: number }) => ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
return <>{children(position)}</>;
}
// Usage: render prop function receives state
<MouseTracker>
{(position) => (
<div
style={{
position: "absolute",
left: position.x,
top: position.y,
width: "20px",
height: "20px",
backgroundColor: "red",
borderRadius: "50%",
}}
/>
)}
</MouseTracker></details>
Higher-order components wrap components to add functionality, state management, or modify props. While hooks are often preferred for logic reuse, HOCs remain valuable for certain patterns.
// HOC for adding authentication check
interface WithAuthProps {
isAuthenticated: boolean;
user: User | null;
}
function withAuth<P extends WithAuthProps>(
Component: React.ComponentType<P>
) {
return function WithAuthComponent(props: Omit<P, keyof WithAuthProps>) {
const [user, setUser] = useState<User | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
// Check auth status
const checkAuth = async () => {
const token = localStorage.getItem("token");
if (token) {
const userData = await api.getCurrentUser();
setUser(userData);
setIsAuthenticated(true);
}
};
checkAuth();
}, []);
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
<Component
{...(props as P)}
isAuthenticated={isAuthenticated}
user={user}
/>
);
};
}
// HOC for form handling
interface WithFormProps<T> {
formState: T;
handleChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
handleSubmit: (e: React.FormEvent) => Promise<void>;
}
function withForm<T extends Record<string, any>, P extends WithFormProps<T>>(
Component: React.ComponentType<P>,
initialState: T
) {
return function WithFormComponent(props: Omit<P, keyof WithFormProps<T>>) {
const [formState, setFormState] = useState(initialState);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormState((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
// Handle submission
} finally {
setIsSubmitting(false);
}
};
return (
<Component
{...(props as P)}
formState={formState}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
);
};
}
// Usage: composing HOCs
const UserForm = withForm(UserProfileComponent, { name: "", email: "" });
const AuthenticatedUserForm = withAuth(UserForm);<details>
<summary>📚 More Examples</summary>
// Example 2: HOC for theme injection
interface WithThemeProps {
theme: "light" | "dark";
}
function withTheme<P extends WithThemeProps>(Component: React.ComponentType<P>) {
return function WithThemeComponent(props: Omit<P, keyof WithThemeProps>) {
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
setTheme(prefersDark ? "dark" : "light");
}, []);
const themeStyles = {
light: { background: "#fff", color: "#000" },
dark: { background: "#000", color: "#fff" },
};
return (
<div style={themeStyles[theme]}>
<Component {...(props as P)} theme={theme} />
</div>
);
};
}</details>
Building complex prop objects programmatically creates flexible, maintainable component interactions. Prop builder functions and composition utilities prevent prop drilling and reduce boilerplate.
// Prop builder for consistency
interface ButtonProps {
variant: "primary" | "secondary" | "danger";
size: "sm" | "md" | "lg";
disabled?: boolean;
onClick?: () => void;
}
// Builder function for common button prop combinations
const buttonPropsBuilder = {
primary: (size: "sm" | "md" | "lg" = "md"): ButtonProps => ({
variant: "primary",
size,
}),
secondary: (size: "sm" | "md" | "lg" = "md"): ButtonProps => ({
variant: "secondary",
size,
}),
danger: (size: "sm" | "md" | "lg" = "md"): ButtonProps => ({
variant: "danger",
size,
}),
};
// Usage: cleaner, more maintainable
<Button {...buttonPropsBuilder.primary("lg")} onClick={handleDelete}>
Delete
</Button>Ready to practice? Challenges | Next: Advanced Props Techniques
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward