Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# RFC: Clarify `_unstable` Naming Convention for Public APIs

---

**Contributors:** @dmytrokirpa

**Stakeholders:**

- Fluent UI React v9 maintainers
- Fluent UI consumers and library users
- Partner teams using Fluent UI components

**Date:** 2026-01-22

**Target end date for feedback:** 2026-02-15

---

## Summary

Deprecate and rename all public API exports with the `_unstable` suffix in stable packages. The current naming creates confusion about API stability guarantees and prevents introducing truly experimental features.

This RFC proposes three coordinated changes:

1. Remove the `_unstable` suffix from stable APIs
2. Maintain backward compatibility through deprecated re-exports
3. Reserve `UNSTABLE_` prefix exclusively for experimental features
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suffix:

  • to avoid "confusion" we should probably go with EXPERIMENTAL_ prefix

experimental api exposure

  • while exposing these directly from existing packages is convenient for both maintainers and consumers it might generate side effects causing issues with bundling and tree shaking in user land. for these reason it might be probably best to expose /experimental api endpoints

experimental api removal/change

  • to not break users we will need to keep this in for 1-2 major version minimum and we cannot change those APIs as well as they are part of STABLE packages. with this in mind besides having a "prefix" to denote to user that this is experimental it acts as stable from semver POV

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suffix:

to avoid "confusion" we should probably go with EXPERIMENTAL_ prefix
experimental api exposure

while exposing these directly from existing packages is convenient for both maintainers and consumers it might generate side effects causing issues with bundling and tree shaking in user land. for these reason it might be probably best to expose /experimental api endpoints

I'm fine with both suggestions of going with EXPERIMENTAL_ prefix and /experimental entry-points for packages

experimental api removal/change

to not break users we will need to keep this in for 1-2 major version minimum and we cannot change those APIs as well as they are part of STABLE packages. with this in mind besides having a "prefix" to denote to user that this is experimental it acts as stable from semver POV

The whole point was to allow having experimental APIs that could be changed/removed and not considering them a part of public contract until they finalized/moved to stable. If we can't establish this convention for some reason then there is no point of introducing this pattern, we just might only need to do clean up and that's it


## Background

Many Fluent UI React v9 stable packages export APIs with `_unstable` suffix (e.g., `useBadge_unstable`, `renderBadge_unstable`). **Currently, ~62 packages export ~1,300+ APIs with this suffix.** These APIs are documented, supported, and follow standard semantic versioning—making the naming misleading. The suffix suggests volatility but the APIs are treated as stable public APIs.

### Historical Context

The `_unstable` suffix was originally introduced to signal lower-level APIs or implementation details during v9's transition from preview to stable. However, as packages graduated to stable status, the suffix remained despite these APIs becoming public, documented, and subject to standard semver guarantees.

## Problem Statement

**Current Issues:**

1. **Misleading naming**: `_unstable` suggests APIs might change at any time, but they follow standard semver guarantees
2. **Blocked innovation**: No clear naming for truly experimental features that can change/be removed without notice
3. **Documentation confusion**: Requires explaining that `_unstable` doesn't mean unstable

**Goals:**

- Establish clear naming that reflects actual stability guarantees
- Enable truly experimental features without confusion
- Maintain backward compatibility during transition
- Provide tooling for smooth migration

**Is this a breaking change?** No—deprecated re-exports maintain full backward compatibility. The removal of deprecated exports in the next major release will be a breaking change, following standard major version semantics.

## Detailed Design or Proposal

### Implementation: Rename and Deprecate

For all exports in stable packages (packages marked as v9 stable, i.e., `packages/react-components/**/src/index.ts`):

**Step 1: Rename primary exports and add deprecated re-exports**

```typescript
// Before
export { useBadge_unstable, renderBadge_unstable, useBadgeStyles_unstable };

// After (new names)
export { useBadge, renderBadge, useBadgeStyles };

// Deprecated re-exports for backward compatibility
/**
* @deprecated Use `useBadge` instead. Will be removed in the next major release.
*/
export const useBadge_unstable = useBadge;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious what's the opinion about doubling our API surface because of this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree that's not ideal scenario, but we need it to make sure everything stays backward compatible


/**
* @deprecated Use `renderBadge` instead. Will be removed in the next major release.
*/
export const renderBadge_unstable = renderBadge;

/**
* @deprecated Use `useBadgeStyles` instead. Will be removed in the next major release.
*/
export const useBadgeStyles_unstable = useBadgeStyles;
```

**Step 2: Update internal usages and documentation**

- Update all internal code to use new names
- Update JSDoc comments and Storybook stories to reference new names
- Update code examples, tutorials, and api.md files to use new names

### Migration Support

Provide automated codemod to help consumers migrate with minimal manual effort:

- **Command**: `npx @fluentui/codemods v9-remove-unstable-suffix`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of scope for this PR but here is the gist how I envision our tools (because codemod package is already taken for v8 related things):

  • we ship @fluentui/cli which would have various sub-commands for various things we might need as we progress

migrate

  • migration defined in json file ( similar approach as nx )
    @fluentui/cli migrate --run-migrations=migrations.json

  • invoking particular migration directly
    @fluentui/cli migrate remove-unstable-suffix

report

@fluentui/cli report


Node           : 22.21.1
OS             : darwin-arm64
Native Target  : aarch64-macos
npm            : 10.9.4

@fluentui/react-components                     : 9.0.1
@fluentui-contrib/foo-bar                 : 0.11.2

stats

  • scans codebase path and generates report for usages of all fluent apis ( imports, props, etc )
    @fluentui/cli stats <path>

agent

  • setups Skills, mcp etc
  • @fluentui/cli agent init

- **Behavior**: Automatically finds and renames all `_unstable` imports and usages
- **Scope**: Works with both named imports and namespace imports across all Fluent UI packages
- **Output**: Generates a migration report showing all changes made
- **Safety**: Preserves code formatting and includes dry-run mode (`--dry-run` flag)

**Example transformations handled by codemod:**

```typescript
// Named imports
import { useBadge_unstable, renderBadge_unstable } from '@fluentui/react-badge';
// ↓ transforms to ↓
import { useBadge, renderBadge } from '@fluentui/react-badge';

// Namespace imports
import * as BadgeUtils from '@fluentui/react-badge';
const hook = BadgeUtils.useBadge_unstable; // ↓ updates reference ↓
const hook = BadgeUtils.useBadge;

// Function calls and type references
const [state] = useBadge_unstable(props);
// ↓ transforms to ↓
const [state] = useBadge(props);
```

**Note**: Detailed codemod implementation, testing strategy, and edge case handling will be covered in a separate RFC.

Automated tooling and a generous deprecation timeline minimize disruption for consumers during the migration.

### Communication Plan

- **Release notes**: Explain change, migration path, and deprecation timeline
- **Contribution guidelines**: Clarify when to use `UNSTABLE_` prefix vs clean names
- **Partner outreach**: Proactive notification to known heavy users before release

### Cleanup (Next Major Release)

- Remove all deprecated `_unstable` re-exports from package entry points
- Update MIGRATION.md with breaking changes documentation
- Final communication to remaining users on deprecated APIs

### Future Convention

**For truly experimental features, use `UNSTABLE_` prefix (all caps):**

```typescript
// Experimental feature - may change or be removed without notice
export const UNSTABLE_useExperimentalFeature = () => {
/* ... */
};
```

**Why `UNSTABLE_` prefix over `_unstable` suffix:**

1. **Highly visible**: All-caps prefix is harder to miss
2. **Clear distinction**: No confusion with deprecated `_unstable` exports
3. **Industry precedent**: Aligns with React's `UNSAFE_` pattern
4. **Intentional friction**: Makes experimental usage explicit

**Criteria for `UNSTABLE_` prefix:**

- Explicitly documented as experimental
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will probably go with @experimental jsdoc annotation right ?

- May change or be removed in any version (including minor/patch)
- Not recommended for production use
- Should include migration path if stabilized or removed

**For stable packages (v9 and later):**

- All stable APIs have clean names without instability markers
- Internal APIs shouldn't be exported; use `@internal` JSDoc tag for documentation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like good time to open again lint rule implementation for no-annotate.

we could also approach this the other way around -> only @public annotated apis can be exported

  • this can be checked already by api extractor and simple failing tool could be written to check this

question is how to properly decouple experimental things to not be used "by accident" in stable apis, which would probably trigger chain of issues when "removing" these experimental ones in next major

- Public APIs should look and feel like first-class citizens

### Scope

**Affected**: All stable packages in `packages/react-components/` (~62 packages, ~1,300+ exports)

- Hooks: `useBadge_unstable` → `useBadge`
- Render functions: `renderBadge_unstable` → `renderBadge`
- Style hooks: `useBadgeStyles_unstable` → `useBadgeStyles`
- Types/Interfaces: Where `_unstable` suffix exists (less common, but should be renamed for consistency)

**Excluded**:

- Preview packages
- Internal implementation details not exported from package entry points
- Experimental features that are genuinely unstable

### Pros and Cons

**Pros:**

- **Clarity**: API names accurately reflect their stability guarantees, eliminating confusion
- **Unblocks innovation**: Clear path for truly experimental features without overloading existing conventions
- **Better DX**: Improved developer experience and discoverability—APIs look like first-class citizens
- **Backward compatible**: Deprecated re-exports ensure existing code continues to work
- **Industry alignment**: Follows common practices (React, Vue, Angular don't use `_unstable` for stable APIs)

**Cons:**

- **Migration effort**: Consumers need to update their code (mitigated by automated codemod)
- **Deprecation warnings**: Users will see warnings until they migrate
- **Temporary bloat**: Duplicate exports temporarily increase bundle size slightly (tree-shaking eliminates unused aliases)
- **Documentation updates**: Need to update all docs, examples, and tutorials
- **Transition confusion**: During deprecation period, both names coexist which may confuse some users

### Risk Mitigation

| Risk | Mitigation |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Consumers ignore deprecation warnings and break on next major release | Provide codemod, clear timeline, ESLint rule, proactive partner outreach |
| Bundle size increase from duplicate exports | Tree-shaking eliminates unused aliases; impact is minimal and temporary |
| Documentation fragmentation during transition | Update all docs immediately to show new names as primary |
| Confusion from coexisting names | Mark old names as deprecated everywhere; IDE autocomplete should prioritize new names |

## Discarded Solutions

### Alternative 1: Do Nothing

This would leave confusion about API stability guarantees and continue blocking innovation. Users would remain uncertain whether `_unstable` APIs are safe to use in production, and the library cannot introduce new truly experimental features without conflicting with existing `_unstable` conventions.

### Alternative 2: Reuse `_unstable` suffix for experimental features

Carries legacy baggage and creates confusion during transition. Since existing `_unstable` APIs are stable and documented, repurposing the suffix for genuinely experimental features would contradict established expectations and invalidate existing documentation without clear migration guidance.

### Alternative 3: Use `_experimental` suffix

Still easy to miss in code and autocomplete, more verbose, and doesn't align with industry precedent. It also creates a third naming convention alongside cleaned-up names, adding rather than reducing cognitive load for developers.

### Alternative 4: Break in next major release only (no deprecation period)

Forces all changes at once, making migration much harder for consumers who would need to update everything simultaneously. A rapid cutover creates support burden and leaves no grace period for those with large codebases.

## Implementation Timeline

- **RFC Approval & Feedback**: 3 weeks (deadline: 2026-02-15)

- Gather core maintainer consensus on approach
- Finalize codemod RFC in parallel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably dont need to proceed with this change with the codemod as dependency

one negative aspect of immediate enablement via "automating" the switch to different symbols would force whole chain of pressure on all teams needing to bump to latest fluent in order to not break runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not going to force anyone switch over. Folks can choose to move to the new APIs if they want to avoid using deprecated APIs, but the old ones will still be available, so nothing will break at runtime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what I meant:

Scenario 1 🚨:

  • there is lib-A/sub-app-A that bumps to latest and applies codemod

  • there is lib-B/app-B` that uses A, but it's on older version of fluent,

  • A runs the codemod and ships new version

  • B is forced to be on the same version ( which should happen eventually ) but if that is not synced -> they would have runtime issue/broken app.

Scenario 2 ✅:

Fluent is part of sandbox env exposing API for devs to provide custom code.

once the sandbox applies codemod all users will all good if sandbox doesn't expose fluent apis in an explicit way where the transform could break the contract

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you mean, but if we're talking about renaming useX_unstable to useX via codemod, then both apps should be fine, as it's a alias to the same thing/reference, and we're keeping both exports - so there shouldn't be any runtime errors.

Copy link
Contributor

@Hotell Hotell Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that works,

i meant this scenario:

// sandbox api / lib / app A
export {useX_unstable } from '@fluentui/react-components' 
// ⬇️
export {useX } from '@fluentui/react-components' 
// user code in sandbox / lib / app B
// 🚨
import {useX_unstable} from '@org/ui'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that works,

i meant this scenario:

// sandbox api / lib / app A
export {useX_unstable } from '@fluentui/react-components' 
// ⬇️
export {useX } from '@fluentui/react-components' 
// user code in sandbox / lib / app B
// 🚨
import {useX_unstable} from '@org/ui'

Are you talking about breaking changes caused by the codemod? I think that's on whoever runs the codemod, not the codemod itself, since it just did its job and moved things away from deprecated APIs.


- **Implementation**: v9.x release cycle (estimated 2026-03 to 2026-04)

- Audit affected APIs and conduct per-package review
- Implement renames and deprecated re-exports across all packages
- Update internal usages, tests, and documentation
- Release in v9.x stable version

- **Migration Period**: v9.x through next major release

- Monitor adoption and address migration issues
- Support partner teams and key consumers
- Gather feedback on codemod effectiveness
- Provide ongoing ESLint rule and documentation support

- **Cleanup (Next Major Release)**:
- Remove all deprecated `_unstable` re-exports
- Update MIGRATION.md with breaking changes
- Final communication to any remaining users

**Dependencies**:

- Codemod RFC approval and parallel implementation
- Partner team coordination and readiness assessment

## Next Steps

If approved, actions will be:

1. **Gather feedback** through 2026-02-15
2. **Draft codemod RFC** as parallel workstream
3. **Assign DRI** for implementation tracking
4. **Create GitHub issue** with detailed implementation checklist
5. **Begin implementation** once all approvals are in place