Frontend Development 15 min read

Mastering React Design Patterns: Composite, Uncontrolled, and Render Props

Explore essential React design patterns—including component composition, uncontrolled composite components, and render props—through detailed explanations, practical code examples, and best practices that improve maintainability, scalability, and developer experience in modern frontend development.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Mastering React Design Patterns: Composite, Uncontrolled, and Render Props

Component Development

As described in "Thinking in React," React changes the way we build web applications by promoting a component‑based development model that is widely used for UI construction.

When developing a UI with React, we typically follow these steps: split the page into components, assign state to each component, and then assemble the components so that data can flow through them.

Split UI into a component tree – Decompose the UI into reusable parts, improving maintainability and extensibility.

Create a static version first – Implement a non‑interactive static page to verify layout and design.

Define the minimal state set – Identify which components need state and keep the state collection minimal.

Locate state in the appropriate component – Place state as close as possible to the components that need it.

Implement reverse data flow – Add interaction so that user actions update state, triggering re‑rendering.

These steps work for most scenarios, but complex or special UIs often require generic solutions or best practices to improve code maintainability, readability, and scalability.

Compound Component Pattern

In a compound component pattern, a parent component contains child components, forming a tree that enables advanced functionality and complex UI. Each child handles its own responsibilities, while the parent coordinates interaction and data transfer, typically sharing implicit state via an explicit parent‑child relationship.

Shared State

The pattern places all UI state logic in the parent component and communicates with children through React Context. The following example shows a plugin system where

ExtensionKit.Provider

and

ExtensionKit.Slot

load and render plugins.

Benefits include minimal changes to the host system (state and logic are hidden inside components) and easier maintenance (adding or removing slots is cheap).

<code>const ExtensionContext = React.createContext();

// ExtensionKit.Provider
const Provider = ({ children, config }) => {
  const [extension, setExtension] = React.useState();

  React.useEffect(() => {
    const extension = fetchExtension(config);
    setExtension(extension);
  }, [config]);

  return (
    <ExtensionContext.Provider value={extension}>
      {React.Children.map(children, child => child)}
    </ExtensionContext.Provider>
  );
};

// ExtensionKit.Slot
const Slot = ({ location }) => {
  const extension = React.useContext(ExtensionContext);
  const { ExtensionUI, data } = extension[location];
  return React.createElement(ExtensionUI, { data });
};

export default { Provider, Slot };
</code>

Conditional Rendering

In some plugin scenarios we require developers to follow strict component conventions (e.g., the outermost component must be

ExtensionUI.Extension

, the title must be wrapped with

ExtensionUI.Header

, etc.). Elements that do not match the allowed

displayName

are ignored.

<code>// ExtensionUI.jsx
const ELEMENT_NAME = ["ExtensionUI.Header"];

// Header component
const Header = props => {
  const { children } = props;
  return <>{children}</>;
};
Header.displayName = "ExtensionUI.Header";

// Extension component
const Extension = props => {
  const { children } = props;
  return (
    <>{React.Children.map(children, child => {
      if (React.isValidElement(child)) {
        const { displayName } = child.type;
        if (ELEMENT_NAME.includes(displayName)) {
          return child;
        }
      }
      return null;
    })}</>
  );
};

export default { Extension, Header };
</code>

Uncontrolled Compound Component

Traditional React data flow follows the DOM tree shape, which can cause deep prop‑drilling. An uncontrolled compound component flips the model: the component tree adapts to the data flow, while the rendered DOM remains unchanged by using portals.

Share element position via Context – Store a DOM reference in context so any component can access the target location.

Ref callback to capture the element – Use a ref callback (not a ref object) to update the context during render.

Portal to the stored location – Render children into the captured DOM node with

ReactDOM.createPortal

.

The following code demonstrates the pattern within the same plugin system.

<code>// ExtensionKit.jsx
const ExtensionContext = React.createContext();
const HeaderRefContext = React.createContext();

const Provider = ({ children, config }) => {
  const [extension, setExtension] = React.useState();
  const [headerRef, setHeaderRef] = React.useState();

  React.useEffect(() => {
    const extension = fetchExtension(config);
    setExtension(extension);
  }, [config]);

  return (
    <ExtensionContext.Provider value={extension}>
      <HeaderRefContext.Provider value={[headerRef, setHeaderRef]}>
        {React.Children.map(children, child => child)}
      </HeaderRefContext.Provider>
    </ExtensionContext.Provider>
  );
};

const Slot = ({ location }) => {
  const extension = React.useContext(ExtensionContext);
  const headerRef = React.useContext(HeaderRefContext);
  const { ExtensionUI, data } = extension[location];
  return React.createElement(ExtensionUI, { data, headerRef });
};

export default { HeaderRefContext, Provider, Slot };
</code>

Host system header component captures its DOM node and stores it in the context:

<code>// Header.jsx
const Header = () => {
  const [, setHeaderRef] = React.useContext(ExtensionKit.HeaderRefContext);
  return <div ref={setHeaderRef} />;
};
</code>

Plugin side renders its header into the captured location using a portal:

<code>// ExtensionUI.jsx
const ELEMENT_NAME = ["ExtensionUI.Header"];

const Header = ({ children, headerRef }) =>
  headerRef ? ReactDOM.createPortal(children, headerRef) : null;
Header.displayName = "ExtensionUI.Header";

const Extension = ({ children }) => (
  <>{React.Children.map(children, child => {
    if (React.isValidElement(child) && ELEMENT_NAME.includes(child.type.displayName)) {
      return child;
    }
    return null;
  })}</>
);

export default { Extension, Header };
</code>

Application composition:

<code>// Host app
const App = () => (
  <ExtensionKit.Provider>
    <Header />
    <Body>
      <ExtensionKit.Slot />
    </Body>
  </ExtensionKit.Provider>
);

// Plugin app
const PluginApp = ({ data, headerRef }) => (
  <ExtensionUI.Extension>
    <ExtensionUI.Header headerRef={headerRef}>This is header …</ExtensionUI.Header>
    <ExtensionUI.Body data={data}>This is body …</ExtensionUI.Body>
  </ExtensionUI.Extension>
);
</code>

Render Props

Render Props pass a render function as a prop, allowing the parent to control UI while the child supplies data. This is useful for plugin‑driven button insertion where the host system defines the button style.

<code>// Extension.ActionSlot
const ActionSlot = ({ renderButton, bottonItems }) => {
  const items = sort(bottonItems);
  return (
    <>
      <Slot />
      {renderButton ? items.map(item => renderButton(item)) : null}
    </>
  );
};
export default ActionSlot;
</code>

Host system supplies the render function:

<code>const App = () => {
  const renderButton = item => {
    const { onClick, text } = item;
    return <button onClick={onClick}>{text}</button>;
  };
  return (
    <ExtensionKit.Provider>
      <Header />
      <Body>
        <ExtensionKit.Slot />
        <div>
          <ExtensionKit.ActionSlot renderButton={renderButton} />
        </div>
      </Body>
    </ExtensionKit.Provider>
  );
};
</code>

Conclusion

This article presented several React design patterns—compound components, uncontrolled compound components, and render props—along with concrete implementations. Applying these patterns helps developers organize and manage complex React applications more effectively, reducing bugs and boosting development efficiency.

Design PatternsFrontend DevelopmentReactPortalContextRender PropsCompound Component
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.