Repository: react
Issue: Bug: useEffect and Event Handler Timing Regressions in React 18 Legacy Mode
Labels: Status: Unconfirmed

## Summary

A change in useEffect and event handler timing causes regressions when upgrading to React 18 in legacy mode (React 18 in concurrent mode doesn't have the regression).

**React version**: 18.3.1 (affects versions since 18.0.0)

## Steps to Reproduce

- Open the [sandbox](https://codesandbox.io/p/sandbox/minimal-report-react-18-legacy-forked-27c5qj). https://codesandbox.io/p/sandbox/minimal-report-react-18-legacy-forked-27c5qj
- Type fast in the input field. Notice that the input does not work properly, and letters are being skipped.

This example uses React 18 in legacy mode. The pattern involves a controlled input directly updating its value through the DOM, which seems to be breaking in React 18 when using legacy mode.

```
useEffect(() => {
    if (inputRef.current) {
      inputRef.current.value = value;
    }
  }, [value]);
```

## Current Behavior

- React 17: Works correctly (baseline).
- React 18 (legacy mode): Inputs break; letters are skipped.
- React 18 (concurrent mode): Works correctly (same as React 17).


## Expected Behavior 

No breaking changes should occur that are specific to React 18 legacy mode. The behavior should be consistent between legacy mode and concurrent mode.

----

### Investigation 

I suspect the issue is related to the following change from the [React 18 upgrade guide](https://react.dev/blog/2022/03/08/react-18-upgrade-guide):

> Other Breaking Changes: consistent useEffect timing: React now always synchronously flushes effect functions if the update was triggered during a discrete user input event such as a click or a keydown event. Previously, the behavior wasn’t always predictable or consistent.

There are no detailed examples I could find to fully understand this change, so I’m not entirely sure if this is the root cause. However, this broken example might indicate unintended behavior in legacy mode.

### Context

We are in the process of upgrading a large React project with many independent `ReactDOM.render` calls to React 18. Our initial plan was to upgrade to React 18 and allow teams to transition to the new renderer independently. However, the upgrade has resulted in several end-to-end test failures, mostly due to this breaking change in the pattern shown in the example.

Two real-world components using this pattern that are affected:

- [React-Monaco-Editor Integration](https://github.com/react-monaco-editor/react-monaco-editor/blob/4a7d7657a6359b648025a8bc30cd7d81e496ecef/src/editor.tsx#L98-L121)
- [Search Box Component in Elastic UI](https://github.com/elastic/eui/blob/b736b904b4c84742bfd9658f588911bceb248f2e/packages/eui/src/components/search_bar/search_box.tsx#L42-L47)


### Workaround

I temporarily resolved the issue by replacing `useEffect` with `useLayoutEffect`. This fixes the problem, but I am unsure if this is the best solution without requiring a significant refactor. I’m also uncertain if this workaround should be applied only in legacy mode while retaining the original useEffect for concurrent mode.

There is also a concern about other possible issues that didn't show up in our test that could have been caused by this change. 

### Ask for Assistance

- Could the team investigate if there is an underlying bug in React 18 legacy mode that needs to be addressed?
- If not, could you please provide more details on this change and suggest guidance on fixing similar patterns or what to watch out for?

Any assistance or guidance on this issue would be greatly appreciated as it is impacting our upgrade path for a large project.