Skip to content

feat(react): add progressive composability - #513

Open
harishsundar-okta wants to merge 1 commit into
mainfrom
feat/composability-implementation
Open

harishsundar-okta wants to merge 1 commit into
mainfrom
feat/composability-implementation

Conversation

@harishsundar-okta

@harishsundar-okta harishsundar-okta commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a four-tier progressive-composability layer across all nine block components, letting hosts adopt composition incrementally with zero breaking changes to existing usage.

Why

Today the block components are all-or-nothing: hosts either use the default component with props, or fork it. There is no supported way to replace a single action, restructure the layout, or drive a fully custom UI while reusing the component's data/logic. This adds a graduated path from "just works" to "fully headless" without breaking the current API.

What

Introduces four tiers of adoption, each opt-in and backwards compatible:

  • Tier 1 — default: <Component {...props} /> — unchanged, fully backwards compatible.
  • Tier 2 — narrow: replace a single action via a render prop (mergeRenderProp chains the host onClick, then the component's command; host preventDefault()/disabled skips it).
  • Tier 3 — structural: compose Root / Header / Content / action / Refresh parts freely and interleave host UI — all parts share one model created once by Root (no duplicate fetch/render).
  • Tier 4 — headless: model hooks re-exported under stable use*Model aliases for fully custom UIs.

Key changes:

  • New shared infra in packages/react/src/lib/composability (createComponentContext, mergeRenderProp).
  • Each component gains a .composable.tsx with its compound parts; the paired *View is re-exported from the base module.
  • actionSlot / hideRefresh seams added to the shared Header and views.
  • Composability test suites for every component.

Per-component capabilities are intentionally not uniform:

Component Tiers Notes
SsoProviderTable 1–4 Pilot; full support
DomainTable 1–4* *Tier-4 create is modal-driven — model exposes state/logic, not a standalone modal component
OrganizationMemberManagement 1–4 Tab-aware refresh
UserPasskeyManagement 1–4 Add action for custom layouts
UserMFAManagement 1, 3, 4 Per-factor enroll — no single action part
OrganizationMemberDetail 1, 3, 4 No Header part (local avatar header)
OrganizationDetailsEdit 1, 3, 4 Save/Cancel are form-internal — no Tier 2
SsoProviderCreate 1, 3, 4 Wizard owns navigation — no Tier 2, takes bundles
SsoProviderEdit 1, 3, 4 Header action is a switch, not a button

Testing: composability suites 43/43 pass; full suite 1887/1887 (zero regressions); tsc --noEmit, lint, and pnpm build all green.

Packages

  • packages/core
  • packages/react

Testing

Member Management - Tier 1

import { OrganizationMemberManagement } from '@auth0/universal-components-react';
import { useNavigate } from 'react-router-dom';

const MemberManagementPage = () => {
  const navigate = useNavigate();

  return (
    <div className="p-6 pt-8 space-y-6">
      <OrganizationMemberManagement
        viewMemberDetailsAction={{
          onAfter: ({ userId, tab }) => {
            navigate(`/member-management/${userId}${tab ? `?tab=${tab}` : ''}`);
          },
        }}
      />
    </div>
  );
};

export default MemberManagementPage;
image

Member Management - Tier 2

import { OrganizationMemberManagement } from '@auth0/universal-components-react';
import { useNavigate } from 'react-router-dom';

const MemberManagementPage = () => {
  const navigate = useNavigate();

  const viewMemberDetailsAction = {
    onAfter: ({ userId, tab }: { userId: string; tab?: string }) => {
      navigate(`/member-management/${userId}${tab ? `?tab=${tab}` : ''}`);
    },
  };

  return (
    <div className="p-6 pt-8 space-y-6">
      <OrganizationMemberManagement.Root viewMemberDetailsAction={viewMemberDetailsAction}>
        <OrganizationMemberManagement.DefaultLayout>
          <OrganizationMemberManagement.InviteAction
            render={
              <button
                type="button"
                className="rounded bg-purple-600 px-4 py-2 text-white"
                onClick={() => console.log('[host] invite clicked')}
              >
                + Invite teammate
              </button>
            }
          />
        </OrganizationMemberManagement.DefaultLayout>
      </OrganizationMemberManagement.Root>
    </div>
  );
};

export default MemberManagementPage;
image

Member Management - Tier 3

import { OrganizationMemberManagement } from '@auth0/universal-components-react';
import { useNavigate } from 'react-router-dom';

const MemberManagementPage = () => {
  const navigate = useNavigate();

  const viewMemberDetailsAction = {
    onAfter: ({ userId, tab }: { userId: string; tab?: string }) => {
      navigate(`/member-management/${userId}${tab ? `?tab=${tab}` : ''}`);
    },
  };

  return (
    <div className="p-6 pt-8 space-y-6">
      <OrganizationMemberManagement.Root viewMemberDetailsAction={viewMemberDetailsAction}>
        <OrganizationMemberManagement.Header />
        <div className="rounded border border-dashed p-4 text-sm text-gray-600">
          Host guidance panel — anything between header and the members table.
        </div>
        <OrganizationMemberManagement.Content />
        <OrganizationMemberManagement.Refresh />
      </OrganizationMemberManagement.Root>
    </div>
  );
};

export default MemberManagementPage;
image

Member Management - Tier4

import { useOrganizationMemberManagementModel } from '@auth0/universal-components-react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const MemberManagementPage = () => {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');

  const model = useOrganizationMemberManagementModel({
    viewMemberDetailsAction: {
      onAfter: ({ userId }) => {
        navigate(`/member-management/${userId}`);
      },
    },
  });

  const submitInvite = (e: React.FormEvent) => {
    e.preventDefault();
    if (!email.trim()) return;
    model.handleCreateSubmit({ invitees: [{ email: email.trim() }] });
    setEmail('');
  };

  return (
    <div className="p-6 pt-8 space-y-6">
      {/* Fully headless invite — host owns the form, model owns the mutation */}
      <form onSubmit={submitInvite} className="flex items-center gap-2">
        <input
          type="email"
          required
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="teammate@example.com"
          className="rounded border px-3 py-2"
        />
        <button
          type="submit"
          disabled={model.isCreatingInvitation}
          className="rounded bg-purple-600 px-4 py-2 text-white disabled:opacity-50"
        >
          {model.isCreatingInvitation ? 'Inviting…' : 'Invite (headless)'}
        </button>
      </form>

      {/* Headless tab switcher */}
      <div className="flex gap-2">
        <button
          className={`rounded px-3 py-1 ${model.activeTab === 'members' ? 'bg-gray-800 text-white' : 'bg-gray-200'}`}
          onClick={() => model.setActiveTab('members')}
        >
          Members
        </button>
        <button
          className={`rounded px-3 py-1 ${model.activeTab === 'invitations' ? 'bg-gray-800 text-white' : 'bg-gray-200'}`}
          onClick={() => model.setActiveTab('invitations')}
        >
          Invitations
        </button>
      </div>

      {model.activeTab === 'members' ? (
        <ul className="list-disc pl-6">
          {model.members.map((m) => (
            <li key={m.user_id}>
              {m.name} — {m.email}{' '}
              <button
                className="underline"
                onClick={() => model.handleViewMemberDetails({ userId: m.user_id ?? '' })}
              >
                details
              </button>
            </li>
          ))}
        </ul>
      ) : (
        <ul className="list-disc pl-6">
          {model.invitations.map((inv) => (
            <li key={inv.id}>{inv.invitee?.email}</li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default MemberManagementPage;

image

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 18cade67-6545-4f4d-aa0b-7eb6a16ea2f7


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@harishsundar-okta harishsundar-okta added the POC Indicates this change is a proof of concept and not production-ready. label Sep 4, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.66135% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.10%. Comparing base (ae4d864) to head (220b41f).

Files with missing lines Patch % Lines
.../my-account/user-passkey-management.composable.tsx 83.62% 19 Missing ⚠️
...0/my-organization/sso-provider-edit.composable.tsx 92.85% 11 Missing ⚠️
...anization/organization-details-edit.composable.tsx 90.09% 10 Missing ⚠️
...tion/organization-member-management.composable.tsx 95.20% 8 Missing ⚠️
...uth0/my-account/user-mfa-management.composable.tsx 93.39% 7 Missing ⚠️
...kages/react/src/components/auth0/shared/header.tsx 93.75% 4 Missing ⚠️
...nization/organization-member-detail.composable.tsx 95.71% 3 Missing ⚠️
...my-organization/sso-provider-create.composable.tsx 97.29% 3 Missing ⚠️
.../my-organization/sso-provider-table.composable.tsx 98.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #513      +/-   ##
==========================================
+ Coverage   90.87%   91.10%   +0.22%     
==========================================
  Files         242      253      +11     
  Lines       18308    19477    +1169     
  Branches     2675     2241     -434     
==========================================
+ Hits        16638    17744    +1106     
- Misses       1670     1733      +63     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@harishsundar-okta harishsundar-okta changed the title feat(react): add progressive composability layer for block components feat(react): add progressive composability Sep 4, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

POC Indicates this change is a proof of concept and not production-ready.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants