# showConfirmDialog React 16 Compatibility Fix Plan

Created: 2026-04-09
Author: smarcet@gmail.com
Status: VERIFIED
Approved: Yes
Iterations: 0
Worktree: No
Type: Bugfix

## Summary

**Symptom:** `showConfirmDialog` causes "Module not found: Error: Can't resolve 'react-dom/client'" build error on React 16 projects, despite a `/* webpackIgnore: true */` dynamic import guard.

**Trigger:** Any consuming project on React 16 that bundles `openstack-uicore-foundation` hits the error during webpack build.

**Root Cause:** `src/components/mui/showConfirmDialog.js:27` — The `import(/* webpackIgnore: true */ "react-dom/client")` compiles into the UMD output as a bare `import("react-dom/client")` (the magic comment is stripped during this library's build). When a consuming project's webpack processes the compiled bundle, it encounters the bare dynamic import, tries to resolve `react-dom/client`, and fails because React 16 has no such module path.

## Investigation

- **Evolution:** Original code (cd39cf3) used only `ReactDOM.render()`. PR #214 (094c6b9) added `require("react-dom/client")` in try/catch for React 18+ support — caused the same build error. Commit 6c95b33 replaced `require()` with `import(/* webpackIgnore: true */ ...)` — the magic comment works for THIS library's webpack but is stripped from the compiled UMD output.
- **Confirmed in built output:** `lib/components/mui/show-confirm-dialog.js` contains literal `await import("react-dom/client")` with no webpack annotation — any consuming webpack resolves it at build time.
- **Key insight:** The `webpackIgnore` comment is a build-time directive that only affects the webpack instance that processes the source file. It does not survive into the output and cannot protect consuming projects.
- **Internal callers** (5 files: mui-table, mui-table-sortable, mui-table-editable, meta-field-values, additional-input-list) all use `showConfirmDialog` as a plain imperative function — `const isConfirmed = await showConfirmDialog({...})`.

## Fix Approach

**Chosen:** Bridge Pattern + Context Provider

**Why:** Completely eliminates all references to `react-dom/client` from the source and compiled output. The dialog renders inside the existing React tree (managed by whatever React version the app uses), so no version-specific rendering API is needed. Preserves the imperative `showConfirmDialog()` API unchanged.

**Alternatives considered:**
- *Configuration injection* (`configureConfirmDialog({ createRoot })`) — simpler but less ergonomic; consumer must handle version detection and import themselves.
- *Remove createRoot entirely* (pure `ReactDOM.render`) — works on 16/17/18 but breaks React 19 where `ReactDOM.render` was removed.

**How it works:**
1. A new `ConfirmDialogProvider` component manages dialog state (open/closed, options, resolve callback) and renders `ConfirmDialog` within the existing React tree.
2. On mount, the provider registers a "bridge" callback in a module-level variable inside `showConfirmDialog.js`.
3. When `showConfirmDialog()` is called, it delegates to the bridge if registered. If no provider is mounted, it falls back to `ReactDOM.render()` with a console warning (backward compat for React 16/17/18 consumers who haven't added the provider yet).
4. Zero references to `react-dom/client` remain in the codebase.

**Files:**
- `src/components/mui/showConfirmDialog.js` — Add bridge registration, fallback logic
- `src/components/mui/ConfirmDialogProvider.js` — NEW: Provider component
- `src/components/index.js` — Export the new provider
- `webpack.common.js` — Add entry point for the new provider

**Tests:**
- `src/components/mui/__tests__/show-confirm-dialog.test.js` — Update for bridge-based flow
- `src/components/mui/__tests__/confirm-dialog-provider.test.js` — NEW: Provider tests

**Defense-in-depth:** Not applicable — this is a build-time resolution error, not a data flow issue.

## Progress

- [x] Task 1: Implement bridge pattern + provider
- [x] Task 2: Verify
      **Tasks:** 2 | **Done:** 2

## Tasks

### Task 1: Implement bridge pattern + provider

**Objective:** Replace the `react-dom/client` dynamic import with a bridge pattern. Create `ConfirmDialogProvider`, modify `showConfirmDialog` to use bridge with ReactDOM.render fallback, export the new provider.

**Files:**
- `src/components/mui/showConfirmDialog.js` — Remove `getCreateRoot()`, add bridge registration exports (`_registerBridge`, `_unregisterBridge`). When bridge is registered, delegate. When not, fall back to `ReactDOM.render()` with `console.warn` suggesting provider migration.
- `src/components/mui/ConfirmDialogProvider.js` — NEW: Functional component using `useState` + `useEffect`. On mount, registers bridge via `_registerBridge`. Bridge callback sets dialog state and returns a Promise. Renders `<ConfirmDialog>` when dialog state is active. On unmount, calls `_unregisterBridge`.
- `src/components/index.js` — Add export: `export { default as ConfirmDialogProvider } from './mui/ConfirmDialogProvider'`
- `webpack.common.js` — Add entry: `'components/mui/confirm-dialog-provider': './src/components/mui/ConfirmDialogProvider.js'`

**TDD:**
1. Write regression test: import `showConfirmDialog` without mocking `react-dom/client` — verify no reference to `react-dom/client` exists in the module
2. Write provider test: render `ConfirmDialogProvider`, call `showConfirmDialog`, verify dialog appears and resolves correctly
3. Write fallback test: call `showConfirmDialog` without provider mounted — verify `ReactDOM.render` is called with console warning
4. Verify all existing tests still pass (callers mock `showConfirmDialog` so they are unaffected)

**Verify:** `npx jest --no-cache`

### Task 2: Verify

**Objective:** Full test suite passes, no regressions across all components that use `showConfirmDialog`.
**Verify:** `npx jest --no-cache && grep -r "react-dom/client" src/ && echo "FAIL: react-dom/client still referenced" || echo "PASS: no react-dom/client references"`
