Skip to content

feat(react-utilities): add composeComponent helper to simplify the component composition#35881

Closed
dmytrokirpa wants to merge 3 commits intomicrosoft:masterfrom
dmytrokirpa:feat/react-utilities/compose-component
Closed

feat(react-utilities): add composeComponent helper to simplify the component composition#35881
dmytrokirpa wants to merge 3 commits intomicrosoft:masterfrom
dmytrokirpa:feat/react-utilities/compose-component

Conversation

@dmytrokirpa
Copy link
Copy Markdown
Contributor

Summary

Adds a composeComponent helper to @fluentui/react-utilities that encapsulates the standard Fluent UI v9 component wiring pattern (useStateuseContextValuesuseStylesrender) into a single, strongly-typed factory call.

Previous Behavior

Every Fluent UI v9 component had to manually write a React.forwardRef wrapper that:

  1. Called a useState hook to derive internal state from props and the forwarded ref.
  2. Optionally called a useContextValues hook to derive context values from state.
  3. Optionally called a useStyles hook to apply CSS-in-JS class names by mutating state.
  4. Called a render function with state (and context values) to produce JSX.

This boilerplate was repeated across every component with no enforced structure or shared abstraction:

const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>((props, ref) => {
  const state = useBadge_unstable(props, ref);
  useBadgeStyles_unstable(state);
  return renderBadge_unstable(state);
});
Badge.displayName = 'Badge';

New Behavior

composeComponent captures the four-step lifecycle as named options and produces a correctly typed ForwardRefComponent:

const Badge = composeComponent({
  displayName: 'Badge',
  useState: useBadge_unstable,
  useStyles: useBadgeStyles_unstable,
  render: renderBadge_unstable,
});

Key properties:

  • Enforced call orderuseState → useContextValues → useStyles → render is guaranteed by the implementation; consumers cannot reorder hooks by mistake.
  • Styled / unstyled split — because useStyles is optional, an unstyled variant can share the same useState and render without any extra wiring:
    const BadgeUnstyled = composeComponent({
      displayName: 'BadgeUnstyled',
      useState: useBadgeBase_unstable, // no variant-specific props
      render: renderBadge_unstable,    // same render function
    });
  • Full generic type support — explicit Element, Props, State, and ContextValues type parameters are supported for consumers that need precise ref types or context shapes.
  • displayName always set — the returned component has displayName assigned, so DevTools and error messages are always readable.

Files changed

File Change
packages/react-components/react-utilities/src/compose/composeComponent.tsx New composeComponent function and ComposeComponentOptions type
packages/react-components/react-utilities/src/compose/index.ts Export composeComponent and ComposeComponentOptions
packages/react-components/react-utilities/src/compose/composeComponent.test.tsx Unit tests (hook pipeline, ref forwarding, useStyles, useContextValues, generic types) + integration tests for styled/unstyled variant pattern

Related Issue(s)

  • Fixes #

@dmytrokirpa dmytrokirpa self-assigned this Mar 19, 2026
@dmytrokirpa dmytrokirpa changed the title feat(react-utilities): add composeComponent helper to simplify the co… feat(react-utilities): add composeComponent helper to simplify the component composition Mar 19, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 19, 2026

📊 Bundle size report

✅ No changes found

@github-actions
Copy link
Copy Markdown

Pull request demo site: URL

@dmytrokirpa dmytrokirpa closed this Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant