Dev Logs
/React/ . Advanced Patterns 🎨
Chapters
  • 01Props & Children: Component Communication Mastery
  • 02useState & Functional State: Making Components Interactive
  • 03Rendering Logic, Lists, and Keys: Dynamic Content Mastery
  • 04Handling DOM Events: User Interactions Mastery
  • 05Controlled vs Uncontrolled Inputs: Form Mastery
  • 06useEffect: Dependencies & Cleanup Mastery
  • 07useRef: Refs vs Variables Deep Dive
  • 08useCallback: Function Memoization
  • 09useContext: Global State Management
  • 10useReducer: Complex State Management
  • 11. Custom Hooks: Reusable Logic 🎣
  • 12. React.memo & Optimization πŸš€
  • 13. Advanced Patterns 🎨
    • Learning Objectives
    • Render Props Pattern
    • Basic Render Props
    • Modern Render Props with Hooks
    • Data Fetching with Render Props
    • Compound Components Pattern
    • Basic Compound Components
    • Advanced Compound Components with Flexible API
    • Higher-Order Components (HOCs)
    • Basic HOC Example
    • Advanced HOC with Configuration
    • Provider Pattern
    • Theme Provider Example
    • State Reducer Pattern
    • Flexible Counter with State Reducer
    • Mini-Challenge: Build a Flexible Form Component
    • Challenge: Create a Compound Form Component
    • Solution: Compound Form Component
    • When to Use Each Pattern
    • Pattern Selection Guide
    • Decision Framework
  • 14. Error Boundaries & Error Handling πŸ›‘οΈ
  • 15. Testing React Applications πŸ§ͺ
  • 16. Performance Optimization ⚑
  • 17. Deployment & Production πŸš€
All chapters

15. Advanced Patterns 🎨

"Good architecture is not about the framework you choose, but about the patterns you apply." - Clean Architecture Principles

🎯 Learning Objectives

By the end of this chapter, you'll understand:

  • Render Props pattern and its use cases
  • Compound Components pattern
  • Higher-Order Components (HOCs)
  • Provider pattern with Context
  • State Reducer pattern
  • Controlled vs Uncontrolled component patterns
  • When and why to use each pattern

🎭 Render Props Pattern

Render Props is a technique for sharing code between components using a prop whose value is a function.

Basic Render Props

jsx
// βœ… Mouse tracker with render props
class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div style={{ height: "100vh" }} onMouseMove={this.handleMouseMove}>
        {/* Call the render prop function with current state */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

// Usage
function App() {
  return (
    <div>
      <h1>Mouse Tracker Demo</h1>

      <MouseTracker
        render={({ x, y }) => (
          <div>
            <h2>
              Mouse position: ({x}, {y})
            </h2>
            <div
              style={{
                position: "absolute",
                left: x - 10,
                top: y - 10,
                width: 20,
                height: 20,
                backgroundColor: "red",
                borderRadius: "50%",
                pointerEvents: "none",
              }}
            />
          </div>
        )}
      />
    </div>
  );
}

Modern Render Props with Hooks

jsx
// βœ… Hook-based render props
function useMouse() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener("mousemove", handleMouseMove);
    return () => window.removeEventListener("mousemove", handleMouseMove);
  }, []);

  return position;
}

// Render props component using the hook
function MouseProvider({ children }) {
  const mousePosition = useMouse();

  return (
    <div>
      {typeof children === "function" ? children(mousePosition) : children}
    </div>
  );
}

// Usage
function App() {
  return (
    <MouseProvider>
      {({ x, y }) => (
        <div>
          <h2>
            Mouse at: ({x}, {y})
          </h2>
          <MouseCursor x={x} y={y} />
        </div>
      )}
    </MouseProvider>
  );
}

function MouseCursor({ x, y }) {
  return (
    <div
      style={{
        position: "fixed",
        left: x - 10,
        top: y - 10,
        width: 20,
        height: 20,
        backgroundColor: "blue",
        borderRadius: "50%",
        pointerEvents: "none",
        zIndex: 9999,
      }}
    />
  );
}

Data Fetching with Render Props

jsx
// βœ… Generic data fetcher
function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isCancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();

        if (!isCancelled) {
          setData(result);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message);
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isCancelled = true;
    };
  }, [url]);

  return children({ data, loading, error });
}

// Usage
function UserProfile({ userId }) {
  return (
    <DataFetcher url={`/api/users/${userId}`}>
      {({ data: user, loading, error }) => {
        if (loading) return <div>Loading user...</div>;
        if (error) return <div>Error: {error}</div>;
        if (!user) return <div>User not found</div>;

        return (
          <div>
            <h2>{user.name}</h2>
            <p>Email: {user.email}</p>
            <p>Role: {user.role}</p>
          </div>
        );
      }}
    </DataFetcher>
  );
}

function PostsList() {
  return (
    <DataFetcher url="/api/posts">
      {({ data: posts, loading, error }) => {
        if (loading) return <div>Loading posts...</div>;
        if (error) return <div>Error: {error}</div>;

        return (
          <div>
            <h2>Posts</h2>
            {posts?.map((post) => (
              <article key={post.id}>
                <h3>{post.title}</h3>
                <p>{post.excerpt}</p>
              </article>
            ))}
          </div>
        );
      }}
    </DataFetcher>
  );
}

🧩 Compound Components Pattern

Compound components work together to form a complete UI. Think of HTML elements like <select> and <option>.

Basic Compound Components

jsx
// βœ… Accordion compound component
const AccordionContext = createContext();

function Accordion({ children, allowMultiple = false }) {
  const [openItems, setOpenItems] = useState(new Set());

  const toggleItem = useCallback(
    (id) => {
      setOpenItems((prev) => {
        const newSet = new Set(prev);

        if (newSet.has(id)) {
          newSet.delete(id);
        } else {
          if (!allowMultiple) {
            newSet.clear();
          }
          newSet.add(id);
        }

        return newSet;
      });
    },
    [allowMultiple]
  );

  const value = useMemo(
    () => ({
      openItems,
      toggleItem,
    }),
    [openItems, toggleItem]
  );

  return (
    <AccordionContext.Provider value={value}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

function AccordionItem({ id, children }) {
  const { openItems, toggleItem } = useContext(AccordionContext);
  const isOpen = openItems.has(id);

  return (
    <div className={`accordion-item ${isOpen ? "open" : ""}`}>
      {React.Children.map(children, (child) => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, { id, isOpen });
        }
        return child;
      })}
    </div>
  );
}

function AccordionHeader({ id, isOpen, children }) {
  const { toggleItem } = useContext(AccordionContext);

  return (
    <button
      className="accordion-header"
      onClick={() => toggleItem(id)}
      aria-expanded={isOpen}
    >
      {children}
      <span className={`accordion-icon ${isOpen ? "rotated" : ""}`}>β–Ό</span>
    </button>
  );
}

function AccordionContent({ isOpen, children }) {
  return (
    <div className={`accordion-content ${isOpen ? "open" : ""}`}>
      <div className="accordion-content-inner">{children}</div>
    </div>
  );
}

// Attach sub-components to main component
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Content = AccordionContent;

// Usage
function App() {
  return (
    <div>
      <h1>FAQ</h1>

      <Accordion allowMultiple={false}>
        <Accordion.Item id="item1">
          <Accordion.Header>What is React?</Accordion.Header>
          <Accordion.Content>
            React is a JavaScript library for building user interfaces. It was
            created by Facebook and is now maintained by Meta.
          </Accordion.Content>
        </Accordion.Item>

        <Accordion.Item id="item2">
          <Accordion.Header>What are React Hooks?</Accordion.Header>
          <Accordion.Content>
            Hooks are functions that let you use state and other React features
            in functional components. They were introduced in React 16.8.
          </Accordion.Content>
        </Accordion.Item>

        <Accordion.Item id="item3">
          <Accordion.Header>What is JSX?</Accordion.Header>
          <Accordion.Content>
            JSX is a syntax extension for JavaScript that looks similar to XML
            or HTML. It allows you to write HTML-like code in your JavaScript
            files.
          </Accordion.Content>
        </Accordion.Item>
      </Accordion>
    </div>
  );
}

Advanced Compound Components with Flexible API

jsx
// βœ… Flexible Modal compound component
const ModalContext = createContext();

function Modal({ children, isOpen, onClose }) {
  const value = useMemo(
    () => ({
      isOpen,
      onClose,
    }),
    [isOpen, onClose]
  );

  useEffect(() => {
    const handleEscape = (e) => {
      if (e.key === "Escape" && isOpen) {
        onClose();
      }
    };

    document.addEventListener("keydown", handleEscape);
    return () => document.removeEventListener("keydown", handleEscape);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <ModalContext.Provider value={value}>
      <div className="modal-overlay" onClick={onClose}>
        <div className="modal-content" onClick={(e) => e.stopPropagation()}>
          {children}
        </div>
      </div>
    </ModalContext.Provider>
  );
}

function ModalHeader({ children }) {
  const { onClose } = useContext(ModalContext);

  return (
    <div className="modal-header">
      <h2>{children}</h2>
      <button className="modal-close" onClick={onClose}>
        Γ—
      </button>
    </div>
  );
}

function ModalBody({ children }) {
  return <div className="modal-body">{children}</div>;
}

function ModalFooter({ children }) {
  return <div className="modal-footer">{children}</div>;
}

// Attach sub-components
Modal.Header = ModalHeader;
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;

// Usage
function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>Open Modal</button>

      <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
        <Modal.Header>Confirm Action</Modal.Header>

        <Modal.Body>
          <p>Are you sure you want to delete this item?</p>
          <p>This action cannot be undone.</p>
        </Modal.Body>

        <Modal.Footer>
          <button onClick={() => setIsModalOpen(false)}>Cancel</button>
          <button
            className="danger"
            onClick={() => {
              // Perform delete action
              console.log("Item deleted");
              setIsModalOpen(false);
            }}
          >
            Delete
          </button>
        </Modal.Footer>
      </Modal>
    </div>
  );
}

🎯 Higher-Order Components (HOCs)

HOCs are functions that take a component and return a new component with additional functionality.

Basic HOC Example

jsx
// βœ… withLoading HOC
function withLoading(WrappedComponent) {
  return function WithLoadingComponent(props) {
    if (props.isLoading) {
      return (
        <div className="loading-spinner">
          <div className="spinner"></div>
          <p>Loading...</p>
        </div>
      );
    }

    return <WrappedComponent {...props} />;
  };
}

// Usage
function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

const UserProfileWithLoading = withLoading(UserProfile);

function App() {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Simulate API call
    setTimeout(() => {
      setUser({ name: "John Doe", email: "john@example.com" });
      setIsLoading(false);
    }, 2000);
  }, []);

  return <UserProfileWithLoading user={user} isLoading={isLoading} />;
}

Advanced HOC with Configuration

jsx
// βœ… withAuth HOC with configuration
function withAuth(WrappedComponent, options = {}) {
  const {
    redirectTo = "/login",
    requiredRoles = [],
    fallbackComponent: FallbackComponent,
  } = options;

  return function WithAuthComponent(props) {
    const { user, isAuthenticated } = useAuth(); // Custom hook

    // Not authenticated
    if (!isAuthenticated) {
      if (FallbackComponent) {
        return <FallbackComponent />;
      }

      // In a real app, you'd use React Router
      window.location.href = redirectTo;
      return null;
    }

    // Check roles if required
    if (requiredRoles.length > 0) {
      const hasRequiredRole = requiredRoles.some((role) =>
        user.roles?.includes(role)
      );

      if (!hasRequiredRole) {
        return (
          <div className="access-denied">
            <h2>Access Denied</h2>
            <p>You don't have permission to view this page.</p>
          </div>
        );
      }
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

// Usage
function AdminPanel({ user }) {
  return (
    <div>
      <h1>Admin Panel</h1>
      <p>Welcome, {user.name}!</p>
      <button>Manage Users</button>
      <button>View Analytics</button>
    </div>
  );
}

function LoginPrompt() {
  return (
    <div>
      <h2>Please log in</h2>
      <button>Login</button>
    </div>
  );
}

const ProtectedAdminPanel = withAuth(AdminPanel, {
  requiredRoles: ["admin"],
  fallbackComponent: LoginPrompt,
});

πŸͺ Provider Pattern

The Provider pattern uses React Context to share data across the component tree.

Theme Provider Example

jsx
// βœ… Theme provider with multiple themes
const ThemeContext = createContext();

const themes = {
  light: {
    background: "#ffffff",
    text: "#000000",
    primary: "#007bff",
    secondary: "#6c757d",
  },
  dark: {
    background: "#121212",
    text: "#ffffff",
    primary: "#bb86fc",
    secondary: "#03dac6",
  },
  blue: {
    background: "#e3f2fd",
    text: "#0d47a1",
    primary: "#1976d2",
    secondary: "#42a5f5",
  },
};

function ThemeProvider({ children }) {
  const [currentTheme, setCurrentTheme] = useState("light");

  const theme = themes[currentTheme];

  const toggleTheme = useCallback(() => {
    setCurrentTheme((prev) => {
      const themeNames = Object.keys(themes);
      const currentIndex = themeNames.indexOf(prev);
      const nextIndex = (currentIndex + 1) % themeNames.length;
      return themeNames[nextIndex];
    });
  }, []);

  const setTheme = useCallback((themeName) => {
    if (themes[themeName]) {
      setCurrentTheme(themeName);
    }
  }, []);

  const value = useMemo(
    () => ({
      theme,
      currentTheme,
      toggleTheme,
      setTheme,
      availableThemes: Object.keys(themes),
    }),
    [theme, currentTheme, toggleTheme, setTheme]
  );

  return (
    <ThemeContext.Provider value={value}>
      <div
        style={{
          backgroundColor: theme.background,
          color: theme.text,
          minHeight: "100vh",
          transition: "all 0.3s ease",
        }}
      >
        {children}
      </div>
    </ThemeContext.Provider>
  );
}

// Custom hook for using theme
function useTheme() {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }

  return context;
}

// Components using the theme
function Header() {
  const { theme, toggleTheme, currentTheme } = useTheme();

  return (
    <header
      style={{
        padding: "1rem",
        borderBottom: `2px solid ${theme.primary}`,
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      }}
    >
      <h1 style={{ color: theme.primary }}>My App</h1>

      <button
        onClick={toggleTheme}
        style={{
          backgroundColor: theme.primary,
          color: theme.background,
          border: "none",
          padding: "0.5rem 1rem",
          borderRadius: "4px",
          cursor: "pointer",
        }}
      >
        Theme: {currentTheme}
      </button>
    </header>
  );
}

function Card({ title, children }) {
  const { theme } = useTheme();

  return (
    <div
      style={{
        border: `1px solid ${theme.secondary}`,
        borderRadius: "8px",
        padding: "1rem",
        margin: "1rem",
        backgroundColor: theme.background,
        boxShadow: `0 2px 4px ${theme.secondary}30`,
      }}
    >
      <h3 style={{ color: theme.primary, marginTop: 0 }}>{title}</h3>
      {children}
    </div>
  );
}

// App using the provider
function App() {
  return (
    <ThemeProvider>
      <Header />

      <main style={{ padding: "1rem" }}>
        <Card title="Welcome">
          <p>This is a themed application!</p>
          <p>Click the theme button to cycle through different themes.</p>
        </Card>

        <Card title="Features">
          <ul>
            <li>Multiple theme support</li>
            <li>Smooth transitions</li>
            <li>Context-based state management</li>
          </ul>
        </Card>
      </main>
    </ThemeProvider>
  );
}

πŸ”„ State Reducer Pattern

The State Reducer pattern gives users control over how state updates work.

Flexible Counter with State Reducer

jsx
// βœ… Counter with customizable state reducer
function useCounter({
  initialValue = 0,
  min = -Infinity,
  max = Infinity,
  step = 1,
  reducer = (state, action) => state, // Default: no-op reducer
} = {}) {
  // Internal reducer that handles the core logic
  const internalReducer = useCallback(
    (state, action) => {
      switch (action.type) {
        case "INCREMENT":
          return Math.min(max, state + step);
        case "DECREMENT":
          return Math.max(min, state - step);
        case "SET":
          return Math.max(min, Math.min(max, action.payload));
        case "RESET":
          return initialValue;
        default:
          return state;
      }
    },
    [min, max, step, initialValue]
  );

  // Combined reducer: user reducer gets first chance to handle actions
  const combinedReducer = useCallback(
    (state, action) => {
      // Let user reducer handle the action first
      const userResult = reducer(state, action);

      // If user reducer returned the same state, use internal reducer
      if (userResult === state) {
        return internalReducer(state, action);
      }

      // User reducer modified state, respect their decision
      return userResult;
    },
    [reducer, internalReducer]
  );

  const [count, dispatch] = useReducer(combinedReducer, initialValue);

  const increment = useCallback(() => {
    dispatch({ type: "INCREMENT" });
  }, []);

  const decrement = useCallback(() => {
    dispatch({ type: "DECREMENT" });
  }, []);

  const set = useCallback((value) => {
    dispatch({ type: "SET", payload: value });
  }, []);

  const reset = useCallback(() => {
    dispatch({ type: "RESET" });
  }, []);

  return {
    count,
    increment,
    decrement,
    set,
    reset,
    dispatch, // Allow direct dispatch for custom actions
  };
}

// Usage examples
function BasicCounter() {
  const counter = useCounter({ initialValue: 10, min: 0, max: 100 });

  return (
    <div>
      <h3>Basic Counter: {counter.count}</h3>
      <button onClick={counter.decrement}>-</button>
      <button onClick={counter.increment}>+</button>
      <button onClick={counter.reset}>Reset</button>
    </div>
  );
}

function CustomCounter() {
  // Custom reducer that adds logging and prevents odd numbers
  const customReducer = useCallback((state, action) => {
    console.log("Counter action:", action.type, "Current state:", state);

    // Prevent setting odd numbers
    if (action.type === "SET" && action.payload % 2 !== 0) {
      console.log("Odd numbers not allowed!");
      return state; // Don't change state
    }

    // Let the default reducer handle other cases
    return state;
  }, []);

  const counter = useCounter({
    initialValue: 0,
    step: 2,
    reducer: customReducer,
  });

  return (
    <div>
      <h3>Custom Counter (Even Numbers Only): {counter.count}</h3>
      <button onClick={counter.decrement}>-2</button>
      <button onClick={counter.increment}>+2</button>
      <button onClick={() => counter.set(7)}>Set to 7 (will fail)</button>
      <button onClick={() => counter.set(8)}>Set to 8 (will work)</button>
      <button onClick={counter.reset}>Reset</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <BasicCounter />
      <hr />
      <CustomCounter />
    </div>
  );
}

🎯 Mini-Challenge: Build a Flexible Form Component

Challenge: Create a Compound Form Component

Build a form system using compound components that supports:

  • Field validation
  • Custom field types
  • Form-level state management
  • Flexible layout
jsx
// Your task: Implement this form system
// Usage should work like this:
function ContactForm() {
  const handleSubmit = (data) => {
    console.log("Form submitted:", data);
  };

  return (
    <Form onSubmit={handleSubmit}>
      <Form.Field name="name" required>
        <Form.Label>Full Name</Form.Label>
        <Form.Input placeholder="Enter your name" />
        <Form.Error />
      </Form.Field>

      <Form.Field name="email" required validate={validateEmail}>
        <Form.Label>Email Address</Form.Label>
        <Form.Input type="email" placeholder="Enter your email" />
        <Form.Error />
      </Form.Field>

      <Form.Field name="message" required>
        <Form.Label>Message</Form.Label>
        <Form.Textarea rows={4} placeholder="Enter your message" />
        <Form.Error />
      </Form.Field>

      <Form.Actions>
        <Form.Reset>Clear</Form.Reset>
        <Form.Submit>Send Message</Form.Submit>
      </Form.Actions>
    </Form>
  );
}

function validateEmail(value) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(value) ? "" : "Please enter a valid email address";
}

Solution: Compound Form Component

jsx
// βœ… Form context and provider
const FormContext = createContext();

function Form({ children, onSubmit, initialValues = {} }) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const setValue = useCallback(
    (name, value) => {
      setValues((prev) => ({ ...prev, [name]: value }));

      // Clear error when user starts typing
      if (errors[name]) {
        setErrors((prev) => ({ ...prev, [name]: "" }));
      }
    },
    [errors]
  );

  const setError = useCallback((name, error) => {
    setErrors((prev) => ({ ...prev, [name]: error }));
  }, []);

  const setTouched = useCallback((name) => {
    setTouched((prev) => ({ ...prev, [name]: true }));
  }, []);

  const validateField = useCallback(
    (name, value, validator) => {
      if (validator) {
        const error = validator(value);
        setError(name, error);
        return !error;
      }
      return true;
    },
    [setError]
  );

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  const handleSubmit = useCallback(
    (e) => {
      e.preventDefault();

      // Validate all fields
      const formData = new FormData(e.target);
      const fieldValidations = [];

      // Get all field validators from form elements
      const fields = e.target.querySelectorAll("[data-field-name]");
      fields.forEach((field) => {
        const name = field.dataset.fieldName;
        const validator = field.dataset.validator;
        const required = field.hasAttribute("required");
        const value = values[name] || "";

        if (required && !value.trim()) {
          setError(name, "This field is required");
          fieldValidations.push(false);
        } else if (validator && window[validator]) {
          const isValid = validateField(name, value, window[validator]);
          fieldValidations.push(isValid);
        } else {
          fieldValidations.push(true);
        }

        setTouched(name);
      });

      // Submit if all validations pass
      if (fieldValidations.every(Boolean)) {
        onSubmit(values);
      }
    },
    [values, validateField, setError, setTouched, onSubmit]
  );

  const value = useMemo(
    () => ({
      values,
      errors,
      touched,
      setValue,
      setError,
      setTouched,
      validateField,
      reset,
    }),
    [
      values,
      errors,
      touched,
      setValue,
      setError,
      setTouched,
      validateField,
      reset,
    ]
  );

  return (
    <FormContext.Provider value={value}>
      <form onSubmit={handleSubmit}>{children}</form>
    </FormContext.Provider>
  );
}

// Field wrapper component
function FormField({ name, required, validate, children }) {
  const { values, errors, touched } = useContext(FormContext);

  const fieldValue = values[name] || "";
  const fieldError = errors[name];
  const fieldTouched = touched[name];
  const showError = fieldTouched && fieldError;

  const fieldContext = useMemo(
    () => ({
      name,
      value: fieldValue,
      error: fieldError,
      touched: fieldTouched,
      showError,
      required,
      validate,
    }),
    [name, fieldValue, fieldError, fieldTouched, showError, required, validate]
  );

  return (
    <FieldContext.Provider value={fieldContext}>
      <div className={`form-field ${showError ? "has-error" : ""}`}>
        {children}
      </div>
    </FieldContext.Provider>
  );
}

const FieldContext = createContext();

// Form input components
function FormLabel({ children }) {
  const { name, required } = useContext(FieldContext);

  return (
    <label htmlFor={name} className="form-label">
      {children}
      {required && <span className="required">*</span>}
    </label>
  );
}

function FormInput({ type = "text", ...props }) {
  const { name, value, required, validate } = useContext(FieldContext);
  const { setValue, setTouched, validateField } = useContext(FormContext);

  const handleChange = (e) => {
    setValue(name, e.target.value);
  };

  const handleBlur = () => {
    setTouched(name);
    if (validate) {
      validateField(name, value, validate);
    }
  };

  return (
    <input
      id={name}
      name={name}
      type={type}
      value={value}
      onChange={handleChange}
      onBlur={handleBlur}
      required={required}
      data-field-name={name}
      data-validator={validate?.name}
      className="form-input"
      {...props}
    />
  );
}

function FormTextarea({ ...props }) {
  const { name, value, required, validate } = useContext(FieldContext);
  const { setValue, setTouched, validateField } = useContext(FormContext);

  const handleChange = (e) => {
    setValue(name, e.target.value);
  };

  const handleBlur = () => {
    setTouched(name);
    if (validate) {
      validateField(name, value, validate);
    }
  };

  return (
    <textarea
      id={name}
      name={name}
      value={value}
      onChange={handleChange}
      onBlur={handleBlur}
      required={required}
      data-field-name={name}
      data-validator={validate?.name}
      className="form-textarea"
      {...props}
    />
  );
}

function FormError() {
  const { showError, error } = useContext(FieldContext);

  if (!showError) return null;

  return <div className="form-error">{error}</div>;
}

function FormActions({ children }) {
  return <div className="form-actions">{children}</div>;
}

function FormSubmit({ children, ...props }) {
  return (
    <button type="submit" className="form-submit" {...props}>
      {children}
    </button>
  );
}

function FormReset({ children, ...props }) {
  const { reset } = useContext(FormContext);

  return (
    <button type="button" onClick={reset} className="form-reset" {...props}>
      {children}
    </button>
  );
}

// Attach sub-components
Form.Field = FormField;
Form.Label = FormLabel;
Form.Input = FormInput;
Form.Textarea = FormTextarea;
Form.Error = FormError;
Form.Actions = FormActions;
Form.Submit = FormSubmit;
Form.Reset = FormReset;

🎯 When to Use Each Pattern

Pattern Selection Guide

PatternUse WhenAvoid When
Render PropsSharing stateful logic, flexible renderingSimple prop passing
Compound ComponentsRelated components that work togetherUnrelated components
HOCsCross-cutting concerns, legacy codeModern React (prefer hooks)
ProviderGlobal state, theme, authLocal component state
State ReducerComplex state logic, user customizationSimple state updates

Decision Framework

What are you trying to achieve?

β”œβ”€ Share stateful logic between components
β”‚  β”œβ”€ Need flexible rendering β†’ Render Props
β”‚  └─ Logic only β†’ Custom Hook
β”‚
β”œβ”€ Create reusable component API
β”‚  β”œβ”€ Components work together β†’ Compound Components
β”‚  └─ Single component β†’ Regular props
β”‚
β”œβ”€ Add functionality to existing components
β”‚  β”œβ”€ Modern React β†’ Custom Hook + Component
β”‚  └─ Legacy/Class components β†’ HOC
β”‚
β”œβ”€ Share data across component tree
β”‚  β”œβ”€ Global state β†’ Provider Pattern
β”‚  └─ Local state β†’ Props/State lifting
β”‚
└─ Complex state management
   β”œβ”€ User needs control β†’ State Reducer
   └─ Internal only β†’ useReducer

Next up: We'll explore error boundaries and learn how to handle errors gracefully in React! πŸ›‘οΈ

Previous. React.memo & Optimization πŸš€Next. Error Boundaries & Error Handling πŸ›‘οΈ

Open source, free forever. Built by iammhador.

Contribute on GitHub