Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
🎁 What are Props?📤 Passing Props to Components🔍 Reading Props in Components🎯 Props for Customization✅ Prop Types & Validation🔄 Props vs State⬇️ Passing Functions as Props🚀 Building Reusable Components
Reactjs/Props/Passing Props

📤 Props Patterns & Composition — Advanced Strategies

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 Pattern

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>

💡 Render Props Pattern

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 (HOC)

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>

🎨 Props Composition and Prop Builders

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>

🔑 Key Takeaways

  • ✅ Compound components eliminate prop drilling through context sharing
  • ✅ Render props enable powerful logic reuse with parent-controlled rendering
  • ✅ HOCs provide component enhancement and prop injection patterns
  • ✅ Composition patterns create flexible, testable architectures
  • ✅ Prop builders reduce boilerplate and ensure consistency
  • ✅ Choose patterns based on use case: hooks for logic, HOCs for enhancement, render props for control

Ready to practice? Challenges | Next: Advanced Props Techniques


Resources

Python Docs

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

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn