react: Bug: React 18 (18.2.0) skips renders in Safari even when props change

It seems that React 18 in Safari is not rendering every render call when the props have changed.

It is possible that I don’t understand something about React 18’s rendering, however, this is not reproducible in React 17 (or in React 18 without createRoot) or in React 18 with Firefox or Chrome, which leads me to think that this is a bug.

React version: 18.2.0

Steps To Reproduce

  • a component that uses state to remember previous props
  • quickly call render on a React18 createRoot and view the app in latest Safari (desktop or mobile)
  • you will see that react sometimes skips renders and the state of the component will be missing some of the props that should have been passed in.

This behavior is not reproducible in React 17 or in React 18 with Firefox or Chrome.

Link to code example: React18SafariSkipRenderPropsChanged is a github repo that demonstrates this bug. You can clone it to reproduce it yourself.

The current behavior

Safari React 18 skips rendering some prop changes so the component misses updates that only happen once.

The expected behavior

All prop changes get rendered so that they can be recorded in state by all components if necessary in Safari.

Please let me know if you have any questions about this bug report, or the attached code repository. I’d love to help explain this bug, or learn what I’m doing wrong. Thanks!

Explanation of linked code repository

This is from React18SafariSkipRenderPropsChanged’s README:

This is either a demonstration of a bug with React 18 (18.2.0) in Safari, or a demonstration of how I don’t quite understand React rendering.

It seems that React 18 in Safari is not always rendering every render call, even if the props have changed. Is this a bug?

This is a very small portion of a React/Typescript/Python game that I’ve been coding. This is a very trimmed down demonstration of the bug.

The game displays a text log of the players’ actions. These messages get sent from the server, usually one at a time. Sometimes these messages arrive in quick succession, ms apart. I don’t have the game server send all of the log messages every time, only the latest message. So the log needs to retain previous messages to have them continue to be displayed to the user.

In this bug demonstration app there are two Components:

  • LogWithState is the simplification of the Log from my game. It uses state to remember previous messages and then loops through all the messages to display them.
  • LogWithoutState takes an array of messages and loops through and displays them all. This is a workaround solution for React 18 rendering in Safari. For this to work, the app remembers the list of previous messages in Typescript not in the React component.

LogWithState uses useState to record the full list of messages in the Log component. This worked fine with React17’s render call. When I upgraded to React18 and createRoot I noticed that log messages were sometimes missing. Recently I discovered that this is not reproducible in Firefox or Chrome, but is easily reproducible in Safari 16.4 and Safari Mobile iOS 16.4.1 (the latest versions as of 2023-04-21) both on my devices and on at least one other device.

The html for this demo has two root nodes for react. We use ReactDOM from React 17 to render into react17-root. We use createRoot and React 18’s render to render into react18-root.

In the React 17 dom we render LogWithState to demonstrate that this works fine.

In the React 18 dom, we render two different components. LogWithState and LogWithoutState.

As you can hopefully see for yourself, “LogWithState - React 17” and “LogWithoutState - React 18” display all 10 of the log messages. “LogWithState - React 18” should be missing some of the logs. Below is an example screenshot on my machine:

screenshot of the bug in action

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 7
  • Comments: 15

Most upvoted comments

Also hitting this issue, safari version 17.1

If use dangerouslySetInnerHTML. It works