Skip to content
Draft
Show file tree
Hide file tree
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
79 changes: 79 additions & 0 deletions docs/react-18-use-insertion-effect-backport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# React 18 `useInsertionEffect` backport

This backport mirrors the stable React 18 client `useInsertionEffect` contract
that can run in React-Luau, ReactNoop, and React Debug Tools. Upstream source
and tests are the contract. Server and renderer packages that React-Luau does
not ship remain explicit capability exclusions.

## Pin and source history

- Stable upstream tag: `v18.0.0`
- Stable upstream commit: `34aa5cfe0d9b6ec4667e02bf46ab34d83dfb2d6d`
- React-Luau base: `9351444c2db37caa08b38ad5de90f438db9221ea`
- Source repository: `facebook/react`

The public seam is `React.useInsertionEffect` rendered through ReactNoop. React
Debug Tools inspection is a second public seam. The behavior-bearing upstream
chain is pinned to:

- `263cfa6ecb9879ecb629d4e04a8c26422b4c4ff9` — runtime, Debug Tools, and tests
- `02f411578a8e58af8ec28e385f6b0dcb768cdc41` — stable public export
- `34aa5cfe0d9b6ec4667e02bf46ab34d83dfb2d6d` — React 18.0.0 contract snapshot

## Supported contract

- create and cleanup in the mutation phase before layout effects
- component-local cleanup/create interleaving
- all insertion effects complete before any layout-effect create
- snapshot lifecycles complete before insertion effects
- pending passive effects flush before a later insertion effect
- dependency-array semantics shared with the other effect hooks
- deletion cleanup and development diagnostics with upstream hook names
- React Debug Tools inspection as `InsertionEffect`

## Source ledger

| Upstream source or test | React-Luau target | Port status | Deviation |
| --- | --- | --- | --- |
| `packages/react/src/ReactHooks.js` — `useInsertionEffect` | `modules/react/src/ReactHooks.lua` | Adapted | Luau arrays and cleanup function types replace Flow. |
| `packages/react/src/React.js` and stable index exports | `modules/react/src/React.lua` | Adapted | React-Luau has one Rojo runtime entry point. |
| `packages/react-reconciler/src/ReactHookEffectTags.js` — `Insertion` | `modules/react-reconciler/src/ReactHookEffectTags.lua` | Direct | Existing internal layout and passive bits shift to their React 18 values. |
| `packages/react-reconciler/src/ReactInternalTypes.js` — hook and dispatcher types | `modules/react-reconciler/src/ReactInternalTypes.lua` and `modules/shared/src/ReactSharedInternals/ReactCurrentDispatcher.lua` | Adapted | The dispatcher type lives in Shared to avoid React-Luau's existing require cycle. |
| `packages/react-reconciler/src/ReactFiberHooks.new.js` — mount/update implementation and dispatchers | `modules/react-reconciler/src/ReactFiberHooks.new.lua` | Adapted | Existing Luau dispatcher tables and effect queue mechanics are retained. |
| `packages/react-reconciler/src/ReactFiberCommitWork.new.js` — insertion create/cleanup and diagnostics | `modules/react-reconciler/src/ReactFiberCommitWork.new.lua` | Adapted | Luau protected calls and warning formatting replace JavaScript calls; order is unchanged. |
| `packages/react-debug-tools/src/ReactDebugHooks.js` — primitive inspection | `modules/react-debug-tools/src/ReactDebugHooks.lua` | Adapted | Luau hook-log tables replace JavaScript objects. Stack parsing accepts Roblox's legacy `LoadedCode` and current `[string "..."]` frame formats; a missing expected ancestor is treated as no shared ancestor. A single Luau `useMemo` result is inspected as its scalar value while multiple returns remain packed. |
| React stable and renderer-specific index variants | No additional target | Out of scope | The single `React.lua` export covers every React-Luau build. |
| `packages/react-dom` server implementation and tests | No target | Out of scope | React-Luau has no DOM or server renderer. |
| `packages/react-server` and `packages/react-suspense-test-utils` exports | No target | Out of scope | React-Luau ships neither package. |

## Test ledger

The reconciler target suite is
`modules/react-reconciler/src/__tests__/useInsertionEffect.spec.lua`. Tests
retain upstream names and order.

| Upstream source or test | React-Luau target | Port status | Deviation |
| --- | --- | --- | --- |
| `ReactHooksWithNoopRenderer-test.js` — `fires insertion effects after snapshots on update` | Same-named Jest-Lua case | Adapted | React-Luau class construction and scheduler matchers only. |
| `fires insertion effects before layout effects` | Same-named Jest-Lua case | Adapted | Scheduler and string construction syntax only. |
| `force flushes passive effects before firing new insertion effects` | Same-named Jest-Lua case | Adapted | React 17's available transition-free scheduling seam exercises the same pending-passive flush. |
| `fires all insertion effects (interleaved) before firing any layout effects` | Same-named Jest-Lua case | Adapted | Luau fragments, arrays, and string construction only. |
| `assumes insertion effect destroy function is either a function or undefined` | Same-named Jest-Lua case | Adapted | Jest-Lua console capture and promises replace their JavaScript equivalents. Luau cannot distinguish an omitted return from an explicit `nil`, so React's `null`-only warning is unrepresentable. Roblox reports the invalid cleanup passed to `xpcall` as an attempted nil call. |
| `ReactHooks-test.internal.js` — invalid dependency-array warning pattern and `ReactFiberHooks.new.js` mount validation | `warns if deps is not an array` | Adapted | Upstream applies `checkDepsAreArrayDev` to `useInsertionEffect` but does not add it to the generic warning test. A focused public-hook regression covers that required dispatcher call. |
| `ReactHooksInspectionIntegration-test.js` — `should inspect the current state of all stateful hooks, including useInsertionEffect` | Same-named case in `ReactHooksInspectionIntegration.spec.lua` | Adapted | Existing React-Luau inspector fixture and Jest-Lua tables are retained. |
| `ReactHooksInspectionIntegration-test.js` — `should inspect custom hooks` | Existing same-named case | Adapted | The upstream hook-tree assertion also locks Roblox's current `[string "..."]` stack-frame parser path. The test module disables Luau optimization because inlining can erase the custom-hook frames this stack-based API inspects. |
| `ReactDOMServerIntegrationHooks-test.js` — server warning | No target | Out of scope | React-Luau has no server renderer. |

## Standalone verification

- The repository-pinned StyLua `0.18.1` formatting check passes.
- Selene `0.28.0` passes with no errors or warnings.
- Luau compiler `0.731` parses and compiles the changed Luau files.
- `bash bin/testing.sh` stops before discovery because `roblox-cli` is not installed. This is a harness prerequisite failure, not a test failure.

## Completion criteria

The backport is complete when every Adapted test passes in an available Roblox
harness, source blocks link to pinned upstream code, standalone formatter and
linter gates pass, unavailable gates have one reproducible prerequisite, and
the React-Luau pull request and working tree are current and clean.
110 changes: 110 additions & 0 deletions docs/react-19.2-use-effect-event-backport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# React 19.2 `useEffectEvent` backport

This backport mirrors the client `React.useEffectEvent` behavior that can run
in React-Luau and ReactNoop. Upstream source and tests are the contract. Server,
DOM, lint-plugin, and hooks that React-Luau does not expose remain explicit
capability exclusions rather than reconstructed substitutes.

## Pin and source history

- Stable upstream tag: `v19.2.0`
- Stable upstream commit: `ae74234eae6ebd62f19190731278e20bc1c37d51`
- Post-release correctness fix: `6bec011b407fe8a2d4babb363289ccce4bc8fcf3`
from React PR `#34831`
- React-Luau base: `9351444c2db37caa08b38ad5de90f438db9221ea`
- Required API base: React 18 `useInsertionEffect` at
`34aa5cfe0d9b6ec4667e02bf46ab34d83dfb2d6d`
- Source repository: `facebook/react`

The public seam is `React.useEffectEvent` rendered through ReactNoop. The
behavior-bearing upstream chain is pinned to:

- `c91a1e03be54733a7dbfcb5663d7a9e8606ab1c1` — initial `useEvent` runtime and tests
- `3517bd9f77dc63189f3bafedf83ba1bf8ae359df` — commit-queue implementation
- `3cc792bfb53c63de34bbb1e8ca131c11faca6cba` — non-stable returned identity
- `84a0a171ea0ecd25e287bd3d3dd30e932beb4677` — `useEffectEvent` rename
- `8bb7241f4c773376893701bfe8b8ff03687342a0` — stable client export
- `6bec011b407fe8a2d4babb363289ccce4bc8fcf3` — `memo` and `forwardRef` commit fix

## Supported contract

- a public hook with the callback's arguments and return values
- access to the latest committed props, state, and context
- callback replacement before layout and passive effects
- a fresh wrapper identity on every render
- render-time invocation errors with upstream text
- multiple Effect Events in one component
- use from custom hooks, function components, `memo`, and `forwardRef`
- no receiver preservation when a method is passed as the callback

## Capability boundary

React-Luau does not expose Fizz, server rendering, React DOM, or the upstream
`eslint-plugin-react-hooks` package. Their Effect Event tests and restrictions
are outside this runtime backport. The stacked React 18 backport supplies
`useInsertionEffect`, so the complete applicable client suite remains in scope.

## Source ledger

| Upstream source or test | React-Luau target | Port status | Deviation |
| --- | --- | --- | --- |
| `packages/react/src/ReactHooks.js` — `useEffectEvent` | `modules/react/src/ReactHooks.lua` | Adapted | Luau variadic type packs replace Flow argument arrays. |
| `packages/react/src/ReactClient.js` and stable index exports | `modules/react/src/React.lua` | Adapted | React-Luau has one Rojo runtime entry point. |
| `packages/shared/ReactFeatureFlags.js` and renderer forks — `enableUseEffectEventHook` | `modules/shared/src/ReactFeatureFlags.lua` | Adapted | React-Luau has one Shared feature-flag module. |
| `packages/react-reconciler/src/ReactInternalTypes.js` — hook and dispatcher types | `modules/react-reconciler/src/ReactInternalTypes.lua` and `modules/shared/src/ReactSharedInternals/ReactCurrentDispatcher.lua` | Adapted | The dispatcher type lives in Shared to avoid React-Luau's existing require cycle. |
| `packages/react-reconciler/src/ReactFiberHooks.js` — event payload, update queue, mount/update hooks, and dispatchers | `modules/react-reconciler/src/ReactFiberHooks.new.lua` | Adapted | Luau arrays, tables, and variadic calls replace JavaScript objects and `Function.apply`. |
| `packages/react-reconciler/src/ReactFiberFlags.js` — `BeforeMutationMask` | `modules/react-reconciler/src/ReactFiberFlags.lua` | Adapted | The single React-Luau build enables Effect Events, so upstream's feature-flagged `Update` bit is baked into the older numeric mask; flag values remain stable. |
| `packages/react-reconciler/src/ReactFiberWorkLoop.js` — invalid render context and before-mutation dispatch | `modules/react-reconciler/src/ReactFiberWorkLoop.new.lua` | Adapted | React 17's commit traversal invokes the existing lifecycle helper for `Update` as well as `Snapshot`. |
| `packages/react-reconciler/src/ReactFiberCommitWork.js` — event payload commit and default snapshot guard | `modules/react-reconciler/src/ReactFiberCommitWork.new.lua` | Adapted | PR `#34831` extends the React 19.2 switch arm to `ForwardRef` and `SimpleMemoComponent`; the obsolete React 17 `Block` tag remains a no-op. The widened before-mutation traversal permits non-snapshot `Update` work on other Fiber tags, matching upstream. |
| `packages/react-debug-tools/src/ReactDebugHooks.js` — Effect Event inspection dispatcher | `modules/react-debug-tools/src/ReactDebugHooks.lua` | Adapted | React-Luau's hook log records the callback with its existing compact entry shape. Stack parsing accepts Roblox's legacy `LoadedCode` and current `[string "..."]` frame formats; a missing expected ancestor is treated as no shared ancestor. A single Luau `useMemo` result is inspected as its scalar value while multiple returns remain packed. |
| `scripts/error-codes/codes.json` — production error `440` | No target | Out of scope | React-Luau ships the full error string and has no production error-code transform. |
| React index build variants | No additional target | Out of scope | The single `React.lua` export covers every React-Luau build. |
| Shared renderer-specific feature-flag forks | No additional target | Out of scope | React-Luau ships one Shared package and one client reconciler. |
| `packages/react-server/src/ReactFizzHooks.js` and Fizz tests | No target | Out of scope | React-Luau does not ship Fizz or a server renderer. |
| `packages/eslint-plugin-react-hooks` Effect Event rules and tests | No target | Out of scope | React-Luau does not ship the JavaScript lint plugin; this is a separate tooling contract. |

## Test ledger

The primary target suite is
`modules/react-reconciler/src/__tests__/useEffectEvent.spec.lua`. The
cross-hook visibility regression remains with `useInsertionEffect.spec.lua`.
Tests retain upstream names and order. Port status describes the required
translation.

| Upstream source or test | React-Luau target | Port status | Deviation |
| --- | --- | --- | --- |
| `useEffectEvent-test.js` — `memoizes basic case correctly` | Same-named Jest-Lua case | Adapted | React-Luau class construction and scheduler matchers only. |
| `can be defined more than once` | Same-named Jest-Lua case | Adapted | React-Luau class construction and scheduler matchers only. |
| `does not preserve \`this\` in event functions` | Same-named Jest-Lua case | Adapted | An extracted Luau method observes a `nil` receiver instead of JavaScript `undefined`. |
| `throws when called in render` | Same-named Jest-Lua case | Adapted | Synchronous scheduler flush replaces `waitForThrow`; error text is unchanged. React-Luau's React 17 recovery renders the sibling in both the concurrent attempt and synchronous retry. |
| `useLayoutEffect shouldn't re-fire when event handlers change` | Same-named Jest-Lua case | Adapted | Scheduler and element construction syntax only. |
| `useEffect shouldn't re-fire when event handlers change` | Same-named Jest-Lua case | Adapted | Scheduler and element construction syntax only. |
| `is stable in a custom hook` | Same-named Jest-Lua case | Adapted | Lua returns the custom-hook pair as multiple values. |
| `is mutated before all other effects` | Same-named Jest-Lua case | Adapted | The stacked React 18 `useInsertionEffect` backport supplies the upstream ordering seam. |
| `doesn't provide a stable identity` | Same-named Jest-Lua case | Adapted | Scheduler and element construction syntax only. |
| `event handlers always see the latest committed value` | Same-named Jest-Lua case | Adapted | ReactNoop's available `act` and output matcher replace async helpers. |
| `integration: implements docs chat room example` | Same-named Jest-Lua case | Adapted | Jest-Lua fake timers and Luau connection table syntax replace JavaScript timers and objects. |
| `integration: implements the docs logVisit example` | Same-named Jest-Lua case | Adapted | Luau arrays and context tables replace JavaScript arrays and JSX. |
| PR `#34831` — `reads the latest context value in memo Components` | Same-named Jest-Lua case | Adapted | Scheduler and element construction syntax only. |
| PR `#34831` — `reads the latest context value in forwardRef Components` | Same-named Jest-Lua case | Adapted | Scheduler and element construction syntax only. |
| `Activity-test.js` — `insertion effects are not disconnected when the visibility changes` | Same-named case in `useInsertionEffect.spec.lua` | Adapted | React-Luau has no Activity API, so `unstable_LegacyHidden` exercises the same Offscreen visibility `Update`. Unlike Activity, LegacyHidden does not retain hidden host output after its deferred children commit an update. |
| React Debug Tools primitive inspection | Existing all-stateful-hooks integration case | Adapted | The React 18 insertion-effect integration case also checks `EffectEvent` and subsequent hook IDs. |
| React Debug Tools custom-hook stack inspection | Existing `should inspect custom hooks` case | Adapted | The test module disables Luau optimization because inlining can erase the custom-hook frames required by the stack-based inspector. |
| `ReactDOMFizzServer-test.js` Effect Event cases | No target | Out of scope | React-Luau has no server rendering or hydration. |

## Standalone verification

- The repository-pinned StyLua `0.18.1` and Selene `0.28.0` pass for the changed Luau files.
- Full-tree formatting passes. Full-tree linting retains the pre-existing
`bin/spec.lua` standard-library error.
- Luau compiler `0.731` parses and compiles all 14 changed Luau files.
- The ReactNoop visibility regression fails on the unguarded invariant and passes mount, hide, hidden update, and reveal after the snapshot guard in Roblox Studio.
- `bash bin/testing.sh` stops before discovery because `roblox-cli` is not installed. This is a harness prerequisite failure, not a test failure.

## Completion criteria

The backport is complete when every Adapted test passes in an available Roblox
harness, every source block links to pinned upstream code, all standalone
formatter and linter gates pass, unavailable standalone gates have one
reproducible prerequisite, and the React-Luau pull request and working tree are
current and clean.
Loading