profile
viewpoint
Andrew Clark acdlite @facebook Redwood City, CA React core at Facebook. Hi!

acdlite/flummox 1702

Minimal, isomorphic Flux.

acdlite/json-sass 90

Transforms a JSON stream into scss syntax Sass.

acdlite/flummox-isomorphic-demo 89

Demo of how to create isomorphic apps using Flummox and react-router

acdlite/jquery.sidenotes 65

Transform Markdown footnotes into superpowered sidenotes

acdlite/change-emitter 48

Listen for changes. Like an event emitter that only emits a single event type. Really tiny.

acdlite/flpunx 34

Better than all the other Flux libraries combined!

acdlite/flummox-immutable-store 17

Flummox store with Immutable.js support for serialization and undo/redo

acdlite/bash-dotfiles 5

.files, including ~/.osx — sensible hacker defaults for OS X

acdlite/andrewphilipclark.com 4

My personal website/blog

push eventacdlite/react

Brian Vaughn

commit sha 9e158c091bb2bd795b734437bf5cead514039531

Updated release script documentation and command names (#17929) * Updated release script documentation and command names * Update scripts/release/README.md Co-Authored-By: Sunil Pai <threepointone@oculus.com> * Updated README Co-authored-by: Sunil Pai <threepointone@oculus.com>

view details

Dominic Gannaway

commit sha df134d31cb107d0dbcdf036d3502a23a81b2c029

Use babel parser rather than Babylon in extract errors (#17988)

view details

Dominic Gannaway

commit sha 256d78d11f1c7da749914a8b2d35b2974a54b0f2

Add feature flag for removing children Map support (#17990)

view details

Dan Abramov

commit sha 3f814e75823dcef29a2f4ee5b22c94830a85a0e2

Fix Flow type for React Native (#17992)

view details

cutjavascript

commit sha 901d76bc5c8dcd0fa15bb32d1dfe05709aa5d273

dataForRoots.set duplicate removal (#17993) dataForRoots.set duplicate removal

view details

Dominic Gannaway

commit sha c55c34e46a6d8148afb78594d14f4675f9346900

Move React Map child check to behind flags or __DEV__ (#17995)

view details

Dan Abramov

commit sha d1bfdfb861dfcb5e9697686bfa37c5b734c0250e

Ignore react-native-web in Flow checks (#17999)

view details

Brian Vaughn

commit sha e05dedc415171aad65e4263ef861b21e867325f7

Added $FlowFixMe to DevTools shell for module we Flow-ignore (#18001)

view details

Sophie Alpert

commit sha 4f71f25a34db0caa1c9c0b75f1f453f948272e65

Re-enable shorthand CSS property collision warning (#18002) Originally added in https://github.com/facebook/react/pull/14181; disabled in https://github.com/facebook/react/pull/14245. Intention was to enable it in React 16.7 but we forgot.

view details

Jesse Katsumata

commit sha 89c6042df373f422bce5744f28597390b6c01ecb

fix: typo in test (#18005)

view details

haseeb

commit sha b63cb6f6cf18bbe28eb0a443c64fd9b87d79616d

Update ReactFiberExpirationTime.js (#17825) replaced 'add' with 'subtract'

view details

Dan Abramov

commit sha 517de74b0c33aa800dbf1f5ce48dc7e336c93cfb

Tweak comment wording (#18007) * Revert "Update ReactFiberExpirationTime.js (#17825)" This reverts commit b63cb6f6cf18bbe28eb0a443c64fd9b87d79616d. * Reword

view details

Dan Abramov

commit sha ab7b83a924bc7c9f43d37beb601d42215916c891

Stop exposing some internals on FB build (#18011)

view details

Dominic Gannaway

commit sha df5faddcc2ad27be5700823d4f5367e5e9ae4620

Refactor commitPlacement to recursively insert nodes (#17996)

view details

Dominic Gannaway

commit sha 42918f40aabd41a324b5dd10652e078dd2c411e6

Change build from babylon to babel (#18015)

view details

Dominic Gannaway

commit sha 529e58ab0a62ed22be9b40bbe44a6ac1b2c89cfe

Remove legacy www config from Rollup build (#18016)

view details

Brian Vaughn

commit sha f7278034de5a289571f26666e6717c4df9f519ad

Flush all passive destroy fns before calling create fns (#17947) * Flush all passive destroy fns before calling create fns Previously we only flushed destroy functions for a single fiber. The reason this is important is that interleaving destroy/create effects between sibling components might cause components to interfere with each other (e.g. a destroy function in one component may unintentionally override a ref value set by a create function in another component). This PR builds on top of the recently added deferPassiveEffectCleanupDuringUnmount kill switch to separate passive effects flushing into two separate phases (similar to layout effects). * Change passive effect flushing to use arrays instead of lists This change offers a small advantage over the way we did things previous: it continues invoking destroy functions even after a previous one errored.

view details

Dominic Gannaway

commit sha 988f4b14eee482e6f73e897cd5329318023e5696

Do not export passiveBrowserEventsSupported from Focus responder (#18022) Remove code

view details

Dan Abramov

commit sha a607ea4c424356707302da998bf13e9bf1b55007

Remove getIsHydrating (#18019)

view details

Sunil Pai

commit sha 58b8797b7372c9296e65e08ce8297e4a394b7972

remove "Unreleased" section from CHANGELOG (#18027) This section is empty, and imo isn't really helpful in React's changelog. I'm honestly not sure why this is even here? Figured I'd start a discussion with a PR, or we can remove it right now.

view details

push time in 15 hours

Pull request review commentreactjs/reactjs.org

Blog post for v16.13.0

+---+title: "React v16.13.0"+author: [threepointone]+---++Today we are releasing React 16.13.0. It contains bugfixes and new deprecation warnings to help prepare for a future major release.++### Warnings for some updates during render++A React render function is supposed to be a pure function. We do support a local update during a render to [derive state from props](https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops). However, this only works on the same Component/Hook. If you call it during a render on a different component, this will now issue a warning.++The fix is usually to move this state change into a `useEffect(...)`.++### Warnings for some deprecated string refs++[String Refs is an old legacy API](https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs) which is discouraged and is going to be deprecated in the future. This release adds a new warning to some unusual edge cases that can't be fixed automatically. For example using a string ref in a render prop:++```jsx+class ClassWithRenderProp extends React.Component {+  componentDidMount() {+    something(this.refs.foo);+  }+  render() {+    return this.props.children();+  }+}++class ClassParent extends React.Component {+  render() {+    return (+      <ClassWithRenderProp>{() => <View ref="foo" />}</ClassWithRenderProp>+    );+  }+}+```++You most likely don't have code like this but if you do, we recommend that you convert it to [`React.createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) instead:++```jsx+class ClassWithRenderProp extends React.Component {+  foo = React.createRef();+  componentDidMount() {+    something(this.foo.current);+  }+  render() {+    return this.props.children(this.foo);+  }+}++class ClassParent extends React.Component {+  render() {+    return (+      <ClassWithRenderProp>{foo => <View ref={foo} />}</ClassWithRenderProp>+    );+  }+}+```++Fixing these edge cases will be a prerequisite to do before being able to apply an automatic codemod.++### Renaming `ReactDOM.unstable_createPortal` to `ReactDOM.createPortal`++Portals are now considered first class supported APIs, and as such we're removing the `unstable_` prefix for the method that creates portals. Replace all calls to `ReactDOM.unstable_createPortal` with `ReactDOM.createPortal`.++### Deprecating `React.createFactory`++[`React.createFactory`](https://reactjs.org/docs/react-api.html#createfactory) is a legacy helper for creating React elements. This release adds a deprecation warning to the method, and will be removed in a future major version. Replace usages of `React.createFactory` with regular JSX.++### Other warnings++- When dynamically changing `style` on a dom element, it's sometimes possible that the applied style clashes with a previously set style, resulting in a badly styled element. React now detects this pattern and logs a warning. The workaround is to pass the same object 'shape' to `style` everytime. ++- The content of a `<textarea>` element should be passed as `value` or `defaultValue` props. An older pattern was to pass `children` to the `<textarea>` element. This release adds a warning for this usage, and support for this will be removed in the next major version.++### Notable bugfixes++This release contains a few other notable improvements:++- In Strict Development Mode, React calls lifecycle methods twice to flush out any possible unwanted side effects. This release adds that behaviour to `shouldComponentUpdate`. This shouldn't affect most code, unless you have side effects in `shouldComponentUpdate`. To fix this, move the code with side effects into `componentDidUpdate`.++- In Strict Development Mode, the warnings for usage of the legacy context API didn't include the component that actually triggered the warning. This release adds the first component stack to the warning.++- Further, this release adds a component stack to a number of warnings that previously didn't include one.++- `onMouseEnter` now correctly gets triggered on disabled `<button>` elements.++- ReactDOM was missing a `version` export since we published v16. This release adds it back. We don't recommend using it in your application logic, but it's useful when debugging issues with mismatching / multiple versions of ReactDOM on the same page.++- The value of `dangerouslySetInnerHTML` was unnecessarily coerced to a string on updates by calling its `toString()`. This release removes that behaviour.++We’re thankful to all the contributors who helped surface and fix these and other issues. You can find the full changelog [below](#changelog).++## Installation {#installation}++### React {#react}++React v16.13.0 is available on the npm registry.++To install React 16 with Yarn, run:++```bash+yarn add react@^16.13.0 react-dom@^16.13.0+```++To install React 16 with npm, run:++```bash+npm install --save react@^16.13.0 react-dom@^16.13.0+```++We also provide UMD builds of React via a CDN:++```html+<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>+<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>+```++Refer to the documentation for [detailed installation instructions](/docs/installation.html).++## Changelog {#changelog}++### React++- Warn when a string ref is used in a manner that's not amenable to a future codemod ([@lunaruan](https://github.com/lunaruan) in [#17864](https://github.com/facebook/react/pull/17864))+- Deprecate `React.createFactory()` ([@trueadm](https://github.com/trueadm) in [#17878](https://github.com/facebook/react/pull/17878))++### React DOM++- Warn when changes in `style` may cause an unexpected collision ([@sophiebits](https://github.com/sophiebits) in [#14181](https://github.com/facebook/react/pull/14181), [#18002](https://github.com/facebook/react/pull/18002))+- Warn when passing `children` to `<textarea>` ([@trueadm](https://github.com/trueadm) in [#17874](https://github.com/facebook/react/pull/17874))+- Warn when a function component is updated during another component's render phase ([@acdlite](<(https://github.com/acdlite)>) in [#17099](https://github.com/facebook/react/pull/17099))+- Deprecate `unstable_createPortal` ([@trueadm](https://github.com/trueadm) in [#17880](https://github.com/facebook/react/pull/17880))+- Flush all passive effect (`useEffect`) destroy functions before calling subsequent create functions ([@bvaughn](https://github.com/bvaughn) in [#17925](https://github.com/facebook/react/pull/17925), [#17947](https://github.com/facebook/react/pull/17947))+- Fix `onMouseEnter` not being fired on disabled buttons ([@AlfredoGJ](https://github.com/AlfredoGJ) in [#17675](https://github.com/facebook/react/pull/17675))+- Call `shouldComponentUpdate` twice when developing in `StrictMode` ([@bvaughn](https://github.com/bvaughn) in [#17942](https://github.com/facebook/react/pull/17942))+- Re-throw errors thrown by the renderer at the root ([@acdlite](<(https://github.com/acdlite)>) in [#18029](https://github.com/facebook/react/pull/18029))

Delete this too, plz

threepointone

comment created time in 2 days

Pull request review commentreactjs/reactjs.org

Blog post for v16.13.0

+---+title: "React v16.13.0"+author: [threepointone]+---++Today we are releasing React 16.13.0. It contains bugfixes and new deprecation warnings to help prepare for a future major release.++### Warnings for some updates during render++A React render function is supposed to be a pure function. We do support a local update during a render to [derive state from props](https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops). However, this only works on the same Component/Hook. If you call it during a render on a different component, this will now issue a warning.++The fix is usually to move this state change into a `useEffect(...)`.++### Warnings for some deprecated string refs++[String Refs is an old legacy API](https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs) which is discouraged and is going to be deprecated in the future. This release adds a new warning to some unusual edge cases that can't be fixed automatically. For example using a string ref in a render prop:++```jsx+class ClassWithRenderProp extends React.Component {+  componentDidMount() {+    something(this.refs.foo);+  }+  render() {+    return this.props.children();+  }+}++class ClassParent extends React.Component {+  render() {+    return (+      <ClassWithRenderProp>{() => <View ref="foo" />}</ClassWithRenderProp>+    );+  }+}+```++You most likely don't have code like this but if you do, we recommend that you convert it to [`React.createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) instead:++```jsx+class ClassWithRenderProp extends React.Component {+  foo = React.createRef();+  componentDidMount() {+    something(this.foo.current);+  }+  render() {+    return this.props.children(this.foo);+  }+}++class ClassParent extends React.Component {+  render() {+    return (+      <ClassWithRenderProp>{foo => <View ref={foo} />}</ClassWithRenderProp>+    );+  }+}+```++Fixing these edge cases will be a prerequisite to do before being able to apply an automatic codemod.++### Renaming `ReactDOM.unstable_createPortal` to `ReactDOM.createPortal`++Portals are now considered first class supported APIs, and as such we're removing the `unstable_` prefix for the method that creates portals. Replace all calls to `ReactDOM.unstable_createPortal` with `ReactDOM.createPortal`.++### Deprecating `React.createFactory`++[`React.createFactory`](https://reactjs.org/docs/react-api.html#createfactory) is a legacy helper for creating React elements. This release adds a deprecation warning to the method, and will be removed in a future major version. Replace usages of `React.createFactory` with regular JSX.++### Deprecating `ReactDOM.unstable_renderSubtreeIntoContainer`++`unstable_renderSubtreeIntoContainer` was an unsupported API for rendering subtrees in `ReactDOM`. The method now logs a warning, and will be removed in a future major version. Please migrate usages of this method to using Portals instead.++### Other warnings++- When dynamically changing `style` on a dom element, it's sometimes possible that the applied style clashes with a previously set style, resulting in a badly styled element. React now detects this pattern and logs a warning. The workaround is to pass the same object 'shape' to `style` everytime. ++- The content of a `<textarea>` element should be passed as `value` or `defaultValue` props. An older pattern was to pass `children` to the `<textarea>` element. This release adds a warning for this usage, and support for this will be removed in the next major version.++### Notable bugfixes++This release contains a few other notable improvements:++- In Strict Development Mode, React calls lifecycle methods twice to flush out any possible unwanted side effects. This release adds that behaviour to `shouldComponentUpdate`. This shouldn't affect most code, unless you have side effects in `shouldComponentUpdate`. To fix this, move the code with side effects into `componentDidUpdate`.++- In Strict Development Mode, the warnings for usage of the legacy context API didn't include the component that actually triggered the warning. This release adds the first component stack to the warning.++- Further, this release adds a component stack to a number of warnings that previously didn't include one.++- `onMouseEnter` now correctly gets triggered on disabled `<button>` elements.++- ReactDOM was missing a `version` export since we published v16. This release adds it back. We don't recommend using it in your application logic, but it's useful when debugging issues with mismatching / multiple versions of ReactDOM on the same page.++- If an error was thrown in the root component, React would go into an infinite loop. This has been fixed to throw that error correctly.

Delete this entry. This bug only affected Fabric. The description makes it sound like the bug could come from user code but it only happened if the error was thrown by the host config.

threepointone

comment created time in 2 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (+    create: () => (() => void) | void,+    deps: Array<mixed> | void | null,+  ) => void,+  useRefImpl: (+    initialValue: MutableSourceRef<Source, Snapshot>,+  ) => {|current: MutableSourceRef<Source, Snapshot>|},+  useStateImpl: (+    initialState: () => MutableSourceState<Source, Snapshot>,+  ) => [MutableSourceState<Source, Snapshot>, Dispatch<any>],+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+): Snapshot {+  const getVersion = source._getVersion;+  const version = getVersion();++  const [state, setState] = useStateImpl(+    () =>+      ({+        getSnapshot,+        snapshot: readFromUnsubcribedMutableSource(source, getSnapshot),+        source,+        subscribe,+      }: MutableSourceState<Source, Snapshot>),+  );++  const ref = useRefImpl({+    getSnapshot: null,+    source: null,+    subscribe: null,+  });++  let didUnsubscribe = false;++  // If we got a new source or subscribe function,+  // we'll need to subscribe in a passive effect,+  // and also check for any changes that fire between render and subscribe.+  useEffectImpl(() => {+    const shouldResubscribe =+      subscribe !== ref.current.subscribe || source !== ref.current.source;++    // We need to re-subscribe any time either the source or the subscribe function changes.+    // We dont' need to re-subscribe when getSnapshot changes,+    // but we will need the latest getSnapshot inside of our change handler-+    // so we store a reference to it the ref as well.+    ref.current = {+      getSnapshot,+      source,+      subscribe,+    };++    if (shouldResubscribe) {+      const handleChange = () => {+        // It's possible that this callback will be invoked even after being unsubscribed,+        // if it's removed as a result of a subscription event/update.+        // In this case, React might log a DEV warning about an update from an unmounted component.+        // We can avoid triggering that warning with this check.+        if (didUnsubscribe) {+          return;+        }++        const latestGetSnapshot = ((ref.current+          .getSnapshot: any): MutableSourceGetSnapshotFn<Source, Snapshot>);+        let newSnapshot = null;+        let snapshotDidThrow = false;+        try {+          newSnapshot = latestGetSnapshot(source._source);+        } catch (error) {+          snapshotDidThrow = true;

Main benefit is not passing a closure in the case where there's no error. If you needed to pass a closure anyway (like to check for stale sources, which I don't think you do for the reasons described in my earlier comment), then this would still have the benefit of saving one extra check from the common path.

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function loadModules({         });       }); +      describe('useMutableSource', () => {

Yeah we should probably start splitting out those tests, too :D

bvaughn

comment created time in 6 days

pull request commentfacebook/react

Don't warn about unmounted updates if pending passive unmount

I'm personally OK with merging this but I do think we should continue to think about the other types of side effects that might occur in an unmount function but aren't a state update

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

Don't warn about unmounted updates if pending passive unmount

 function loadModules({               ]);             });           });++          it('does not warn about state updates for unmounted components with pending passive unmounts', () => {+            let completePendingRequest = null;+            function Component() {+              Scheduler.unstable_yieldValue('Component');+              const [didLoad, setDidLoad] = React.useState(false);+              React.useLayoutEffect(() => {+                Scheduler.unstable_yieldValue('layout create');+                return () => {+                  Scheduler.unstable_yieldValue('layout destroy');+                };+              }, []);+              React.useEffect(() => {+                Scheduler.unstable_yieldValue('passive create');+                // Mimic an XHR request with a complete handler that updates state.+                completePendingRequest = () => setDidLoad(true);+                return () => {+                  Scheduler.unstable_yieldValue('passive destroy');+                };+              }, []);+              return didLoad;+            }++            act(() => {+              ReactNoop.renderToRootWithID(<Component />, 'root', () =>+                Scheduler.unstable_yieldValue('Sync effect'),+              );+              expect(Scheduler).toFlushAndYieldThrough([+                'Component',+                'layout create',+                'Sync effect',+              ]);+              ReactNoop.flushPassiveEffects();+              expect(Scheduler).toHaveYielded(['passive create']);++              // Unmount but don't process pending passive destroy function+              ReactNoop.unmountRootWithID('root');+              expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);++              // Simulate an XHR completing, which will cause a state update-+              // but should not log a warning.+              completePendingRequest();++              ReactNoop.flushPassiveEffects();+              expect(Scheduler).toHaveYielded(['passive destroy']);+            });+          });++          it('still warns about state updates for unmounted components with no pending passive unmounts', () => {

Can you add a test for when there's a pending unmount on a different component?

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

Don't warn about unmounted updates if pending passive unmount

 function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {       // Only warn for user-defined components, not internal ones like Suspense.       return;     }++    if (+      deferPassiveEffectCleanupDuringUnmount &&+      runAllPassiveEffectDestroysBeforeCreates+    ) {+      // If there are pending passive effects unmounts for this Fiber,+      // we can assume that they would have prevented this update.+      if (pendingPassiveHookEffectsUnmount.includes(fiber)) {

Nit: includes is a new-ish API, so use indexof or something instead so this doesn't break.

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (

Did you try doing the thing we discussed where you read the correct implementation from the dispatcher object? I know that will require a bit of refactoring to deal with things like currentHookNameInDev but I think we should deal with that now, both to avoid this factory function and so we can use the same strategy for useTransition, useOpaqueIdentifier, and other first-class composite hooks in the future.

Alternatively, I would go back to the forked implementations and factor the repeated parts into functions.

But also might be worth leaving to the end to avoid churn.

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (+    create: () => (() => void) | void,+    deps: Array<mixed> | void | null,+  ) => void,+  useRefImpl: (+    initialValue: MutableSourceRef<Source, Snapshot>,+  ) => {|current: MutableSourceRef<Source, Snapshot>|},+  useStateImpl: (+    initialState: () => MutableSourceState<Source, Snapshot>,+  ) => [MutableSourceState<Source, Snapshot>, Dispatch<any>],+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+): Snapshot {+  const getVersion = source._getVersion;+  const version = getVersion();++  const [state, setState] = useStateImpl(+    () =>+      ({+        getSnapshot,+        snapshot: readFromUnsubcribedMutableSource(source, getSnapshot),+        source,+        subscribe,+      }: MutableSourceState<Source, Snapshot>),+  );++  const ref = useRefImpl({+    getSnapshot: null,+    source: null,+    subscribe: null,+  });++  let didUnsubscribe = false;

I think you meant to put this inside the useEffect body

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (+    create: () => (() => void) | void,+    deps: Array<mixed> | void | null,+  ) => void,+  useRefImpl: (+    initialValue: MutableSourceRef<Source, Snapshot>,+  ) => {|current: MutableSourceRef<Source, Snapshot>|},+  useStateImpl: (+    initialState: () => MutableSourceState<Source, Snapshot>,+  ) => [MutableSourceState<Source, Snapshot>, Dispatch<any>],+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+): Snapshot {+  const getVersion = source._getVersion;+  const version = getVersion();++  const [state, setState] = useStateImpl(+    () =>+      ({+        getSnapshot,+        snapshot: readFromUnsubcribedMutableSource(source, getSnapshot),+        source,+        subscribe,+      }: MutableSourceState<Source, Snapshot>),+  );++  const ref = useRefImpl({+    getSnapshot: null,+    source: null,+    subscribe: null,+  });++  let didUnsubscribe = false;++  // If we got a new source or subscribe function,+  // we'll need to subscribe in a passive effect,+  // and also check for any changes that fire between render and subscribe.+  useEffectImpl(() => {+    const shouldResubscribe =+      subscribe !== ref.current.subscribe || source !== ref.current.source;++    // We need to re-subscribe any time either the source or the subscribe function changes.+    // We dont' need to re-subscribe when getSnapshot changes,+    // but we will need the latest getSnapshot inside of our change handler-+    // so we store a reference to it the ref as well.+    ref.current = {+      getSnapshot,+      source,+      subscribe,+    };++    if (shouldResubscribe) {+      const handleChange = () => {+        // It's possible that this callback will be invoked even after being unsubscribed,+        // if it's removed as a result of a subscription event/update.+        // In this case, React might log a DEV warning about an update from an unmounted component.+        // We can avoid triggering that warning with this check.+        if (didUnsubscribe) {

Never mind, just saw your other PR! 😍

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (+    create: () => (() => void) | void,+    deps: Array<mixed> | void | null,+  ) => void,+  useRefImpl: (+    initialValue: MutableSourceRef<Source, Snapshot>,+  ) => {|current: MutableSourceRef<Source, Snapshot>|},+  useStateImpl: (+    initialState: () => MutableSourceState<Source, Snapshot>,+  ) => [MutableSourceState<Source, Snapshot>, Dispatch<any>],+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+): Snapshot {+  const getVersion = source._getVersion;+  const version = getVersion();++  const [state, setState] = useStateImpl(+    () =>+      ({+        getSnapshot,+        snapshot: readFromUnsubcribedMutableSource(source, getSnapshot),+        source,+        subscribe,+      }: MutableSourceState<Source, Snapshot>),+  );++  const ref = useRefImpl({+    getSnapshot: null,+    source: null,+    subscribe: null,+  });++  let didUnsubscribe = false;++  // If we got a new source or subscribe function,+  // we'll need to subscribe in a passive effect,+  // and also check for any changes that fire between render and subscribe.+  useEffectImpl(() => {+    const shouldResubscribe =+      subscribe !== ref.current.subscribe || source !== ref.current.source;++    // We need to re-subscribe any time either the source or the subscribe function changes.+    // We dont' need to re-subscribe when getSnapshot changes,+    // but we will need the latest getSnapshot inside of our change handler-+    // so we store a reference to it the ref as well.+    ref.current = {+      getSnapshot,+      source,+      subscribe,+    };++    if (shouldResubscribe) {+      const handleChange = () => {+        // It's possible that this callback will be invoked even after being unsubscribed,+        // if it's removed as a result of a subscription event/update.+        // In this case, React might log a DEV warning about an update from an unmounted component.+        // We can avoid triggering that warning with this check.+        if (didUnsubscribe) {

Can you elaborate more on why this warning is not legit? Is this the issue where the mutation event happens while there are still pending passive effects?

If it's only that, let's skip this check and solve holistically in a separate task/PR. Perhaps by wrapping the warning in a check for pending passive unmounts, as we've discussed in the past.

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function loadModules({         });       }); +      describe('useMutableSource', () => {

Nit: Can we move these to their own test suite? I don't typically like new files but this test suite is getting to be quite slow.

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (+    create: () => (() => void) | void,+    deps: Array<mixed> | void | null,+  ) => void,+  useRefImpl: (+    initialValue: MutableSourceRef<Source, Snapshot>,+  ) => {|current: MutableSourceRef<Source, Snapshot>|},+  useStateImpl: (+    initialState: () => MutableSourceState<Source, Snapshot>,+  ) => [MutableSourceState<Source, Snapshot>, Dispatch<any>],+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+): Snapshot {+  const getVersion = source._getVersion;+  const version = getVersion();++  const [state, setState] = useStateImpl(+    () =>+      ({+        getSnapshot,+        snapshot: readFromUnsubcribedMutableSource(source, getSnapshot),+        source,+        subscribe,+      }: MutableSourceState<Source, Snapshot>),+  );++  const ref = useRefImpl({+    getSnapshot: null,+    source: null,+    subscribe: null,+  });++  let didUnsubscribe = false;++  // If we got a new source or subscribe function,+  // we'll need to subscribe in a passive effect,+  // and also check for any changes that fire between render and subscribe.+  useEffectImpl(() => {+    const shouldResubscribe =+      subscribe !== ref.current.subscribe || source !== ref.current.source;++    // We need to re-subscribe any time either the source or the subscribe function changes.+    // We dont' need to re-subscribe when getSnapshot changes,+    // but we will need the latest getSnapshot inside of our change handler-+    // so we store a reference to it the ref as well.+    ref.current = {

Since this is a first class hook, let's cheat a bit 😆

Instead of using a commit effect to keep these current, you can assign these to the work-in-progress hook during the render phase. They will be become "current" when the work-in-progress commits, via the atomic swap. I'm calling this a "cheat" because this capability doesn't really exist in userspace, except via the local-setState-in-render pattern.

bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  snapshot: Snapshot,+  source: MutableSource<any>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+|};++function readFromUnsubcribedMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+): Snapshot {+  const root = ((getWorkInProgressRoot(): any): FiberRoot);+  invariant(+    root !== null,+    'Expected a work-in-progress root. This is a bug in React. Please file an issue.',+  );++  const getVersion = source._getVersion;+  const version = getVersion();++  // Is it safe for this component to read from this source during the current render?+  let isSafeToReadFromSource = false;++  // Check the version first.+  // If this render has already been started with a specific version,+  // we can use it alone to determine if we can safely read from the source.+  const currentRenderVersion = getWorkInProgressVersion(source);+  if (currentRenderVersion !== null) {+    isSafeToReadFromSource = currentRenderVersion === version;+  } else {+    // If there's no version, then we should fallback to checking the update time.+    const pendingExpirationTime = getPendingExpirationTime(root, source);++    if (pendingExpirationTime === NoWork) {+      isSafeToReadFromSource = true;+    } else {+      // If the source has pending updates, we can use the current render's expiration+      // time to determine if it's safe to read again from the source.+      isSafeToReadFromSource =+        pendingExpirationTime === NoWork ||+        pendingExpirationTime >= renderExpirationTime;+    }++    if (isSafeToReadFromSource) {+      // If it's safe to read from this source during the current render,+      // store the version in case other components read from it.+      // A changed version number will let those components know to throw and restart the render.+      setWorkInProgressVersion(source, version);+    }+  }++  if (isSafeToReadFromSource) {+    return getSnapshot(source._source);+  } else {+    invariant(+      false,+      'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',+    );+  }+}++type MutableSourceRef<Source, Snapshot> = {|+  getSnapshot: null | MutableSourceGetSnapshotFn<Source, Snapshot>,+  source: null | MutableSource<Source>,+  subscribe: null | MutableSourceSubscribeFn<Source, Snapshot>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  useEffectImpl: (+    create: () => (() => void) | void,+    deps: Array<mixed> | void | null,+  ) => void,+  useRefImpl: (+    initialValue: MutableSourceRef<Source, Snapshot>,+  ) => {|current: MutableSourceRef<Source, Snapshot>|},+  useStateImpl: (+    initialState: () => MutableSourceState<Source, Snapshot>,+  ) => [MutableSourceState<Source, Snapshot>, Dispatch<any>],+  source: MutableSource<Source>,+  getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,+  subscribe: MutableSourceSubscribeFn<Source, Snapshot>,+): Snapshot {+  const getVersion = source._getVersion;+  const version = getVersion();++  const [state, setState] = useStateImpl(+    () =>+      ({+        getSnapshot,+        snapshot: readFromUnsubcribedMutableSource(source, getSnapshot),+        source,+        subscribe,+      }: MutableSourceState<Source, Snapshot>),+  );++  const ref = useRefImpl({+    getSnapshot: null,+    source: null,+    subscribe: null,+  });++  let didUnsubscribe = false;++  // If we got a new source or subscribe function,+  // we'll need to subscribe in a passive effect,+  // and also check for any changes that fire between render and subscribe.+  useEffectImpl(() => {+    const shouldResubscribe =+      subscribe !== ref.current.subscribe || source !== ref.current.source;++    // We need to re-subscribe any time either the source or the subscribe function changes.+    // We dont' need to re-subscribe when getSnapshot changes,+    // but we will need the latest getSnapshot inside of our change handler-+    // so we store a reference to it the ref as well.+    ref.current = {+      getSnapshot,+      source,+      subscribe,+    };++    if (shouldResubscribe) {+      const handleChange = () => {+        // It's possible that this callback will be invoked even after being unsubscribed,+        // if it's removed as a result of a subscription event/update.+        // In this case, React might log a DEV warning about an update from an unmounted component.+        // We can avoid triggering that warning with this check.+        if (didUnsubscribe) {+          return;+        }++        const latestGetSnapshot = ((ref.current+          .getSnapshot: any): MutableSourceGetSnapshotFn<Source, Snapshot>);+        let newSnapshot = null;+        let snapshotDidThrow = false;+        try {+          newSnapshot = latestGetSnapshot(source._source);+        } catch (error) {+          snapshotDidThrow = true;

Interesting...

I would do this instead:

} catch (error) {
  setState(() => {
    throw error;
  });
}
bvaughn

comment created time in 6 days

Pull request review commentfacebook/react

useMutableSource hook

 function loadModules({         });       }); +      describe('useMutableSource', () => {+        const defaultGetSnapshot = source => source.value;+        const defaultSubscribe = (source, callback) =>+          source.subscribe(callback);++        function createComplexSource(initialValueA, initialValueB) {+          const callbacksA = [];+          const callbacksB = [];+          let revision = 0;+          let valueA = 'a:one';+          let valueB = 'b:one';++          const subscribeHelper = (callbacks, callback) => {+            if (callbacks.indexOf(callback) < 0) {+              callbacks.push(callback);+            }+            return () => {+              const index = callbacks.indexOf(callback);+              if (index >= 0) {+                callbacks.splice(index, 1);+              }+            };+          };++          return {+            subscribeA(callback) {+              return subscribeHelper(callbacksA, callback);+            },+            subscribeB(callback) {+              return subscribeHelper(callbacksB, callback);+            },++            get listenerCountA() {+              return callbacksA.length;+            },+            get listenerCountB() {+              return callbacksB.length;+            },++            set valueA(newValue) {+              revision++;+              valueA = newValue;+              callbacksA.forEach(callback => callback());+            },+            get valueA() {+              return valueA;+            },++            set valueB(newValue) {+              revision++;+              valueB = newValue;+              callbacksB.forEach(callback => callback());+            },+            get valueB() {+              return valueB;+            },++            get version() {+              return revision;+            },+          };+        }++        function createSource(initialValue) {+          const callbacks = [];+          let revision = 0;+          let value = initialValue;+          return {+            subscribe(callback) {+              if (callbacks.indexOf(callback) < 0) {+                callbacks.push(callback);+              }+              return () => {+                const index = callbacks.indexOf(callback);+                if (index >= 0) {+                  callbacks.splice(index, 1);+                }+              };+            },+            get listenerCount() {+              return callbacks.length;+            },+            set value(newValue) {+              revision++;+              value = newValue;+              callbacks.forEach(callback => callback());+            },+            get value() {+              return value;+            },+            get version() {+              return revision;+            },+          };+        }++        function createMutableSource(source) {+          return React.createMutableSource(source, () => source.version);+        }++        function Component({getSnapshot, label, mutableSource, subscribe}) {+          const snapshot = React.useMutableSource(+            mutableSource,+            getSnapshot,+            subscribe,+          );+          Scheduler.unstable_yieldValue(`${label}:${snapshot}`);+          return <div>{`${label}:${snapshot}`}</div>;+        }++        it('should subscribe to a source and schedule updates when it changes', () => {+          const source = createSource('one');+          const mutableSource = createMutableSource(source);++          act(() => {+            ReactNoop.renderToRootWithID(+              <>+                <Component+                  label="a"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+                <Component+                  label="b"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+              </>,+              'root',+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYieldThrough([+              'a:one',+              'b:one',+              'Sync effect',+            ]);++            // Subscriptions should be passive+            expect(source.listenerCount).toBe(0);+            ReactNoop.flushPassiveEffects();+            expect(source.listenerCount).toBe(2);++            // Changing values should schedule an update with React+            source.value = 'two';+            expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']);++            // Umounting a component should remove its subscriptino.+            ReactNoop.renderToRootWithID(+              <>+                <Component+                  label="a"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+              </>,+              'root',+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']);+            ReactNoop.flushPassiveEffects();+            expect(source.listenerCount).toBe(1);++            // Umounting a root should remove the remaining event listeners+            ReactNoop.unmountRootWithID('root');+            expect(Scheduler).toFlushAndYield([]);+            ReactNoop.flushPassiveEffects();+            expect(source.listenerCount).toBe(0);++            // Changes to source should not trigger an updates or warnings.+            source.value = 'three';+            expect(Scheduler).toFlushAndYield([]);+          });+        });++        it('should restart work if a new source is mutated during render', () => {+          const source = createSource('one');+          const mutableSource = createMutableSource(source);++          act(() => {+            ReactNoop.render(+              <>+                <Component+                  label="a"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+                <Component+                  label="b"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+              </>,+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );++            // Do enough work to read from one component+            expect(Scheduler).toFlushAndYieldThrough(['a:one']);++            // Mutate source before continuing work+            source.value = 'two';++            // Render work should restart and the updated value should be used+            expect(Scheduler).toFlushAndYield([+              'a:two',+              'b:two',+              'Sync effect',+            ]);+          });+        });++        it('should schedule an update if a new source is mutated between render and commit (subscription)', () => {+          const source = createSource('one');+          const mutableSource = createMutableSource(source);++          act(() => {+            ReactNoop.render(+              <>+                <Component+                  label="a"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+                <Component+                  label="b"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+              </>,+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );++            // Finish rendering+            expect(Scheduler).toFlushAndYieldThrough([+              'a:one',+              'b:one',+              'Sync effect',+            ]);++            // Mutate source before subscriptions are attached+            expect(source.listenerCount).toBe(0);+            source.value = 'two';++            // Mutation should be detected, and a new render should be scheduled+            expect(Scheduler).toFlushAndYield(['a:two', 'b:two']);+          });+        });++        it('should unsubscribe and resubscribe if a new source is used', () => {+          const sourceA = createSource('a-one');+          const mutableSourceA = createMutableSource(sourceA);++          const sourceB = createSource('b-one');+          const mutableSourceB = createMutableSource(sourceB);++          act(() => {+            ReactNoop.render(+              <Component+                label="only"+                getSnapshot={defaultGetSnapshot}+                mutableSource={mutableSourceA}+                subscribe={defaultSubscribe}+              />,+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYieldThrough([+              'only:a-one',+              'Sync effect',+            ]);+            ReactNoop.flushPassiveEffects();+            expect(sourceA.listenerCount).toBe(1);++            // Changing values should schedule an update with React+            sourceA.value = 'a-two';+            expect(Scheduler).toFlushAndYield(['only:a-two']);++            // If we re-render with a new source, the old one should be unsubscribed.+            ReactNoop.render(+              <Component+                label="only"+                getSnapshot={defaultGetSnapshot}+                mutableSource={mutableSourceB}+                subscribe={defaultSubscribe}+              />,+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYield([+              'only:a-two', // (replayed)+              'only:b-one',+              'Sync effect',+            ]);+            ReactNoop.flushPassiveEffects();+            expect(sourceA.listenerCount).toBe(0);+            expect(sourceB.listenerCount).toBe(1);++            // Changing to original source should not schedule updates with React+            sourceA.value = 'a-three';+            expect(Scheduler).toFlushAndYield([]);++            // Changing new source value should schedule an update with React+            sourceB.value = 'b-two';+            expect(Scheduler).toFlushAndYield(['only:b-two']);+          });+        });++        it('should unsubscribe and resubscribe if a new subscribe function is provided', () => {+          const source = createSource('a-one');+          const mutableSource = createMutableSource(source);++          const unsubscribeA = jest.fn();+          const subscribeA = jest.fn(s => {+            const unsubscribe = defaultSubscribe(s);+            return () => {+              unsubscribe();+              unsubscribeA();+            };+          });+          const unsubscribeB = jest.fn();+          const subscribeB = jest.fn(s => {+            const unsubscribe = defaultSubscribe(s);+            return () => {+              unsubscribe();+              unsubscribeB();+            };+          });++          act(() => {+            ReactNoop.renderToRootWithID(+              <Component+                label="only"+                getSnapshot={defaultGetSnapshot}+                mutableSource={mutableSource}+                subscribe={subscribeA}+              />,+              'root',+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);+            ReactNoop.flushPassiveEffects();+            expect(source.listenerCount).toBe(1);+            expect(subscribeA).toHaveBeenCalledTimes(1);++            // If we re-render with a new subscription function,+            // the old unsubscribe function should be called.+            ReactNoop.renderToRootWithID(+              <Component+                label="only"+                getSnapshot={defaultGetSnapshot}+                mutableSource={mutableSource}+                subscribe={subscribeB}+              />,+              'root',+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYield([+              'only:a-one',+              // Reentrant render to update state with new subscribe function.+              'only:a-one',+              'Sync effect',+            ]);+            ReactNoop.flushPassiveEffects();+            expect(source.listenerCount).toBe(1);+            expect(unsubscribeA).toHaveBeenCalledTimes(1);+            expect(subscribeB).toHaveBeenCalledTimes(1);++            // Unmounting should call the newer unsunscribe.+            ReactNoop.unmountRootWithID('root');+            expect(Scheduler).toFlushAndYield([]);+            ReactNoop.flushPassiveEffects();+            expect(source.listenerCount).toBe(0);+            expect(unsubscribeB).toHaveBeenCalledTimes(1);+          });+        });++        it('should re-use previously read snapshot value when reading is unsafe', () => {+          const source = createSource('one');+          const mutableSource = createMutableSource(source);++          act(() => {+            ReactNoop.render(+              <>+                <Component+                  label="a"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+                <Component+                  label="b"+                  getSnapshot={defaultGetSnapshot}+                  mutableSource={mutableSource}+                  subscribe={defaultSubscribe}+                />+              </>,+              () => Scheduler.unstable_yieldValue('Sync effect'),+            );+            expect(Scheduler).toFlushAndYield([+              'a:one',+              'b:one',+              'Sync effect',+            ]);++            // Changing values should schedule an update with React.+            // Start working on this update but don't finish it.+            source.value = 'two';+            expect(Scheduler).toFlushAndYieldThrough(['a:two']);++            // Re-renders that occur before the udpate is processed+            // should reuse snapshot so long as the config has not changed+            ReactNoop.flushSync(() => {

I see you have a TODO below to add more tests around concurrent updates. I would suggest using either ReactNoop.discreteUpdates or Scheduler's runWithPriority for most of these tests, instead of flushSync. The reason is that flushSync always synchronously flushes right at the end, but the more general case is a concurrent update at a slightly higher priority. I don't think this will drastically change your existing test cases, just a suggestion.

bvaughn

comment created time in 6 days

issue commentfacebook/react

Concurrent Mode and UseSubscription with RxJS "lose" updates

@samcooke98 Thank you for that PR! I feel a bit guilty that it got buried and I didn't notice it. But I really appreciate that you took the time to write a unit test 😮We'll try to do a better job keeping track of our PR queue!

samcooke98

comment created time in 6 days

issue closedfacebook/react

[Concurrent mode] Scheduled updates are lost when parent state update happens

Do you want to request a feature or report a bug? Probably a bug

What is the current behavior? When in concurrent mode (render with unstable_createRoot), triggering a state change via an event (eg click) on the parent makes React loose the scheduled updates in the children. See:

https://codesandbox.io/s/react-concurrent-state-bug-sp6zq

Not an expert on concurrent mode here, was just playing around with it. Noticed that when some interactions happen the work (probably moved to the other tree) gets lost if the parent re-renders. You can see by clicking anywhere in the sandbox page vs clicking on the button that changes some local state. But that is just my educated guess on what might be happening.

What is the expected behavior?

State updates that are scheduled in children components should happen and the DOM value should reflect the state.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

react: 16.10.2 react-dom: 16.10.2

closed time in 6 days

albertogasparin

issue commentfacebook/react

[Concurrent mode] Scheduled updates are lost when parent state update happens

Thanks for the bug report!

I believe this was fixed by https://github.com/facebook/react/pull/18091

You can confirm using react@0.0.0-experimental-ea6ed3dbb and react-dom@react@0.0.0-experimental-ea6ed3dbb.

I'm going to close this issue, but if the bug persists please comment and we'll reopen.

albertogasparin

comment created time in 6 days

issue closedfacebook/react

Concurrent Mode and UseSubscription with RxJS "lose" updates

<!-- Note: if the issue is about documentation or the website, please file it at: https://github.com/reactjs/reactjs.org/issues/new -->

Do you want to request a feature or report a bug? Bug - I think?

What is the current behavior?

In Concurrent Mode, it appears that if a render is interrupted, if a component is using useSubscription the interrupted update is lost, which leads to "tearing"

The following codesandbox uses useSubscription with a RxJS BehaviorSubject, mimicking the example from here: https://www.npmjs.com/package/use-subscription#subscribing-to-observables

In the sandbox, clicking on the "Increment Remote Count" button triggers the RxJs BehaviorSubject to increment. This is done outside of the React event handler (ie: via window.addEventLIstener and so the updates are not batched together. The update to render the numbers is artificially slowed down.

If you click the "Increment Remote Count" button multiple times, the update works as expected.

If you interrupt the update, via clicking the "increment local count", only the last number will update.

So the Steps to reproduce look like:

  1. Click the "Increment Remote Count" button once
  2. Before the update is committed to the DOM, click the "Increment Local Count" update.
  3. The first update is "lost" ie; the output looks like:

image

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

https://jwenc.csb.app/ \ https://codesandbox.io/s/usesubscriptionconcurrentlosingupdates-jwenc

What is the expected behavior?

I'd expect there to be a commit as the above screenshot, but I'd then expect there to be a follow-up commit that restores the consistency. In other words, I'd expect in the above picture for everything to be 1

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

0.0.0-experimental-f6b8d31a7

I'd be willing to try to take a stab at writing a React test for this, if needed?

closed time in 6 days

samcooke98

issue commentfacebook/react

Concurrent Mode and UseSubscription with RxJS "lose" updates

Thanks for the bug report!

I believe this was fixed by https://github.com/facebook/react/pull/18091

You can confirm using react@0.0.0-experimental-ea6ed3dbb and react-dom@react@0.0.0-experimental-ea6ed3dbb.

I'm going to close this issue, but if the bug persists please comment and we'll reopen.

samcooke98

comment created time in 6 days

issue closedfacebook/react

useTransition - startTransition does not work on React.memo when is SimpleMemoComponent

<!-- Note: if the issue is about documentation or the website, please file it at: https://github.com/reactjs/reactjs.org/issues/new -->

Do you want to request a feature or report a bug? bug What is the current behavior? useTransition - startTransition do not work on React.memo when is SimpleMemoComponent

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

What is the expected behavior? startTransition work with SimpleMemoComponent

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

closed time in 6 days

salvoravida

issue commentfacebook/react

useTransition - startTransition does not work on React.memo when is SimpleMemoComponent

Thanks for the bug report!

I believe this was fixed by https://github.com/facebook/react/pull/18091

You can confirm using react@0.0.0-experimental-ea6ed3dbb and react-dom@react@0.0.0-experimental-ea6ed3dbb.

I'm going to close this issue, but if the bug persists please comment and we'll reopen.

salvoravida

comment created time in 6 days

pull request commentfacebook/react

Bugfix: `memo` drops lower pri updates on bail out

@dai-shi Yes probably. I'll confirm in a sec.

acdlite

comment created time in 6 days

push eventfacebook/react

Andrew Clark

commit sha 5de5b61507d44c158fc0223728c5834fbd224ec5

Bugfix: `memo` drops lower pri updates on bail out (#18091) Fixes a bug where lower priority updates on a components wrapped with `memo` are sometimes left dangling in the queue without ever being processed, if they are preceded by a higher priority bailout. Cause ----- The pending update priority field is cleared at the beginning of `beginWork`. If there is remaining work at a lower priority level, it's expected that it will be accumulated on the work-in-progress fiber during the begin phase. There's an exception where this assumption doesn't hold: SimpleMemoComponent contains a bailout that occurs *before* the component is evaluated and the update queues are processed, which means we don't accumulate the next priority level. When we complete the fiber, the work loop is left to believe that there's no remaining work. Mitigation ---------- Since this only happens in a single case, a late bailout in SimpleMemoComponent, I've mitigated the bug in that code path by restoring the original update priority from the current fiber. This same case does not apply to MemoComponent, because MemoComponent fibers do not contain hooks or update queues; rather, they wrap around an inner fiber that may contain those. However, I've added a test case for MemoComponent to protect against a possible future regression. Possible next steps ------------------- We should consider moving the update priority assignment in `beginWork` out of the common path and into each branch, to avoid similar bugs in the future.

view details

push time in 6 days

PR merged facebook/react

Bugfix: `memo` drops lower pri updates on bail out CLA Signed React Core Team

Fixes a bug where lower priority updates on a components wrapped with memo are sometimes left dangling in the queue without ever being processed, if they are preceded by a higher priority bailout.

Cause

The pending update priority field is cleared at the beginning of beginWork. If there is remaining work at a lower priority level, it's expected that it will be accumulated on the work-in-progress fiber during the begin phase.

There's an exception where this assumption doesn't hold: SimpleMemoComponent contains a bailout that occurs before the component is evaluated and the update queues are processed, which means we don't accumulate the next priority level. When we complete the fiber, the work loop is left to believe that there's no remaining work.

Mitigation

Since this only happens in a single case, a late bailout in SimpleMemoComponent, I've mitigated the bug in that code path by restoring the original update priority from the current fiber.

This same case does not apply to MemoComponent, because MemoComponent fibers do not contain hooks or update queues; rather, they wrap around an inner fiber that may contain those. However, I've added a test case for MemoComponent to protect against a possible future regression.

Possible next steps

We should consider moving the update priority assignment in beginWork out of the common path and into each branch, to avoid similar bugs in the future.

+88 -1

3 comments

2 changed files

acdlite

pr closed time in 6 days

PR opened facebook/react

Reviewers
Bugfix: `memo` drops lower pri updates on bail out

Fixes a bug where lower priority updates on a components wrapped with memo are sometimes left dangling in the queue without ever being processed, if they are preceded by a higher priority bailout.

Cause

The pending update priority field is cleared at the beginning of beginWork. If there is remaining work at a lower priority level, it's expected that it will be accumulated on the work-in-progress fiber during the begin phase.

There's an exception where this assumption doesn't hold: SimpleMemoComponent contains a bailout that occurs before the component is evaluated and the update queues are processed, which means we don't accumulate the next priority level. When we complete the fiber, the work loop is left to believe that there's no remaining work.

Mitigation

Since this only happens in a single case, a late bailout in SimpleMemoComponent, I've mitigated the bug in that code path by restoring the original update priority from the current fiber.

This same case does not apply to MemoComponent, because MemoComponent fibers do not contain hooks or update queues; rather, they wrap around an inner fiber that may contain those. However, I've added a test case for MemoComponent to protect against a possible future regression.

Possible next steps

We should consider moving the update priority assignment in beginWork out of the common path and into each branch, to avoid similar bugs in the future.

+88 -1

0 comment

2 changed files

pr created time in 6 days

create barnchacdlite/react

branch : bugfixmemo

created branch time in 6 days

PR merged facebook/react

Reviewers
Warn for update on different component in render CLA Signed React Core Team

This warning already exists for class components, but not for functions.

It does not apply to render phase updates to the same component, which have special semantics that we do support.

+98 -29

6 comments

3 changed files

acdlite

pr closed time in 8 days

push eventfacebook/react

Andrew Clark

commit sha ea6ed3dbbd32515e2d4d9783c358beceeadd4b1d

Warn for update on different component in render (#17099) This warning already exists for class components, but not for functions. It does not apply to render phase updates to the same component, which have special semantics that we do support.

view details

push time in 8 days

push eventacdlite/react

Krzysztof Kotowicz

commit sha fdba0e5ce75f1cf1f82f2937f351bbfdcaadbcc5

Fixed a bug with illegal invocation for Trusted Types (#17083) * Fixed a bug with illegal invocation. * Fixed the test.

view details

Dominic Gannaway

commit sha e7704e22a146c2d5b00f8c6bfa6255153272f053

[babel-plugin-react-jsx] Avoid duplicate "children" key in props object (#17094) * [babel-plugin-react-jsx] Avoid duplicate "children" key in props object * Use Object.assign approach

view details

Dominic Gannaway

commit sha 4cb399a433771c84e861d5ca3d38a24733d23ad8

[react-interactions] Modify Scope query mechanism (#17095)

view details

Andrew Clark

commit sha 30c5daf943bd3bed38e464ac79e38f0e8a27426b

Remove concurrent apis from stable (#17088) * Tests run in experimental mode by default For local development, you usually want experiments enabled. Unless the release channel is set with an environment variable, tests will run with __EXPERIMENTAL__ set to `true`. * Remove concurrent APIs from stable builds Those who want to try concurrent mode should use the experimental builds instead. I've left the `unstable_` prefixed APIs in the Facebook build so we can continue experimenting with them internally without blessing them for widespread use. * Turn on SSR flags in experimental build * Remove prefixed concurrent APIs from www build Instead we'll use the experimental builds when syncing to www. * Remove "canary" from internal React version string

view details

Andrew Clark

commit sha 9123c479f4f3a21f897a923aeb33bf0499f4891f

Enable concurrent APIs in all experimental forks (#17102) Forgot to update the flags in the forked modules.

view details

Andrew Clark

commit sha 43562455c992cfc614f4301473443e1360baf7b5

Temporary patch www fork with prefixed APIs (#17103) I'm doing this here instead of in the downstream repo so that if the sync diff gets reverted, it doesn't revert this, too. Once the sync has landed, and the callers are updated in www, I will remove this.

view details

Luna Ruan

commit sha 3ac0eb075d82b19a912c6665eb4f6adb9245cbcb

Modify Babel React JSX Duplicate Children Fix (#17101) If a JSX element has both a children prop and children (ie. <div children={childOne}>{childTwo}</div>), IE throws an Multiple definitions of a property not allowed in strict mode. This modifies the previous fix (which used an Object.assign) by making the duplicate children a sequence expression on the next prop/child instead so that ordering is preserved. For example: ``` <Component children={useA()} foo={useB()} children={useC()}>{useD()}</Component> ``` should compile to ``` React.jsx(Component, {foo: (useA(), useB()), children: (useC(), useD)}) ```

view details

Andrew Clark

commit sha 2c832b4dcfa5406be1b16c03c78198e84380fc2e

Separate sizebot for experimental builds (#17100) Configures the sizebot to leave a second comment that tracks the experimental build artifacts.

view details

Sebastian Markbåge

commit sha 6ff23f2a5da0e60fa008cef97529469945618d06

Change retry priority to "Never" for dehydrated boundaries (#17105) This changes the "default" retryTime to NoWork which schedules at Normal pri. Dehydrated bouundaries normally hydrate at Never priority except when they retry where we accidentally increased them to Normal because Never was used as the default value. This changes it so NoWork is the default. Dehydrated boundaries however get initialized to Never as the default. Therefore they now hydrate as Never pri unless their priority gets increased by a forced rerender or selective hydration. This revealed that erroring at this Never priority can cause an infinite rerender. So I fixed that too.

view details

Andrew Clark

commit sha d7feeb25ac8cc23133c294fa01932658023130c0

unstable_createRoot -> createRoot in test (#17107) Fixes test added in #17105, which was based on an earler commit than the one that removed the `unstable_` prefix from `createRoot`.

view details

Dominic Gannaway

commit sha 916937563b4a71f2a61a75699f06122a544f4542

[react-interactions] Add onFocusWithin event to FocusWithin responder (#17115)

view details

Sebastian Markbåge

commit sha ed5f010ae51db1544ce92e1a5105e870b5a5098e

Client render Suspense content if there's no boundary match (#16945) Without the enableSuspenseServerRenderer flag there will never be a boundary match. Also when it is enabled, there might not be a boundary match if something was conditionally rendered by mistake. With this PR it will now client render the content of a Suspense boundary in that case and issue a DEV only hydration warning. This is the only sound semantics for this case. Unfortunately, landing this will once again break #16938. It will be less bad though because at least it'll just work by client rendering the content instead of hydrating and issue a DEV only warning. However, we must land this before enabling the enableSuspenseServerRenderer flag since it does this anyway. I did notice that we special case fallback={undefined} due to our unfortunate semantics for that. So technically a workaround that works is actually setting the fallback to undefined on the server and during hydration. Then flip it on only after hydration. That could be a workaround if you want to be able to have a Suspense boundary work only after hydration for some reason. It's kind of unfortunate but at least those semantics are internally consistent. So I added a test for that.

view details

Andrew Clark

commit sha 7cec15155ac2bb9d8c61dc8ab1951c6226c5cd3b

Remove prefixed concurrent APIs from www build (#17108) The downstream callers have been updated, so we can remove these.

view details

Dominic Gannaway

commit sha 8facc0537390bbdd52359f693ecf445e6f787174

[react-interactions] Allow event.preventDefault on LegacyPress responder (#17113) [react-interactions] Allow event.preventDefault on LegacyPress responder

view details

Nicolas Gallagher

commit sha 4fb5bf61dd52473330fb5b3fdf0ffc091054a28c

[react-interactions] Fix focus-visible heuristic (#17124) Respond to all keys not just `Tab`

view details

Sebastian Markbåge

commit sha 4eeee358e12c1408a4b40830bb7bb6956cf26b47

[SuspenseList] Store lastEffect before rendering (#17131) * Add a failing test for SuspenseList bug * Store lastEffect before rendering We can't reset the effect list to null because we don't rereconcile the children so we drop deletion effects if we do that. Instead we store the last effect as it was before we started rendering so we can go back to where it was when we reset it. We actually already do something like this when we delete the last row for the tail="hidden" mode so we had a field available for it already.

view details

Dan Abramov

commit sha 0b61e26983c20b0a4facb50857133cde4a28555f

Update RN typings for a shim (#17138)

view details

Luna Ruan

commit sha 685ed561f22ea062281a4c570c7067e6020457c4

Migrate useDeferredValue and useTransition (#17058) Migrated useDeferredValue and useTransition from Facebook's www repo into ReactFiberHooks.

view details

Andrew Clark

commit sha c47f59331ee94b1d04f974f075373d368a8c8ab3

Move SuspenseList to experimental package (#17130) Also moves `withSuspenseConfig`

view details

Andrew Clark

commit sha 7082d5a2db5c1e5f49a62aecd90b6858f957da5e

Don't build non-experimental www bundles (#17139) Reduces the likelihood we'll accidentally sync the wrong ones.

view details

push time in 8 days

push eventfacebook/react

Andrew Clark

commit sha 4d9f8500651c5d1e19d8ec9a2359d5476a53814b

Re-throw errors thrown by the renderer at the root in the complete phase (#18029) * Re-throw errors thrown by the renderer at the root React treats errors thrown at the root as a fatal because there's no parent component that can capture it. (This is distinct from an "uncaught error" that isn't wrapped in an error boundary, because in that case we can fall back to deleting the whole tree -- not great, but at least the error is contained to a single root, and React is left in a consistent state.) It turns out we didn't have a test case for this path. The only way it can happen is if the renderer's host config throws. We had similar test cases for host components, but none for the host root. This adds a new test case and fixes a bug where React would keep retrying the root because the `workInProgress` pointer was not advanced to the next fiber. (Which in this case is `null`, since it's the root.) We could consider in the future trying to gracefully exit from certain types of root errors without leaving React in an inconsistent state. For example, we should be able to gracefully exit from errors thrown in the begin phase. For now, I'm treating it like an internal invariant and immediately exiting. * Add comment

view details

push time in 8 days

PR merged facebook/react

Re-throw errors thrown by the renderer at the root in the complete phase CLA Signed React Core Team

React treats errors thrown at the root as a fatal because there's no parent component that can capture it. (This is distinct from an "uncaught error" that isn't wrapped in an error boundary, because in that case we can fall back to deleting the whole tree – not great, but at least the error is contained to a single root, and React is left in a consistent state.)

It turns out we didn't have a test case for this path. The only way it can happen is if the renderer's host config throws. We had similar test cases for host components, but none for the host root.

This adds a new test case and fixes a bug where React would keep retrying the root because the workInProgress pointer was not advanced to the next fiber. (Which in this case is null, since it's the root.)

We could consider in the future trying to gracefully exit from certain types of root errors without leaving React in an inconsistent state. For example, we should be able to gracefully exit from errors thrown in the begin phase. For now, I'm treating it like an internal invariant and immediately exiting.

+26 -0

4 comments

3 changed files

acdlite

pr closed time in 8 days

CommitCommentEvent
CommitCommentEvent

push eventacdlite/react

Andrew Clark

commit sha 348302e8dc231a77789bb5815020035e0bddbb9b

Add comment

view details

push time in 8 days

Pull request review commentfacebook/react

Re-throw errors thrown by the renderer at the root in the complete phase

 function handleError(root, thrownValue) {         // boundary.         workInProgressRootExitStatus = RootFatalErrored;         workInProgressRootFatalError = thrownValue;+        workInProgress = null;

Ok I'll add one! I think there's a potential follow up to also call unwindWork here to pop the contexts. I originally didn't bother because it's not impossible even that call will throw, but if layout errors are common enough it might be worth doing more to prevent corrupted internal state.

acdlite

comment created time in 8 days

push eventfacebook/react

Andrew Clark

commit sha 56d8a73affad624ee4d48f1685e0a92adce0bd9c

[www] Disable Scheduler `timeout` w/ dynamic flag (#18069) Before attempting to land an expiration times refactor, I want to see if this particular change will impact performance (either positively or negatively). I will test this with a GK.

view details

push time in 9 days

PR merged facebook/react

[www] Disable Scheduler `timeout` w/ dynamic flag CLA Signed React Core Team

Before attempting to land an expiration times refactor, I want to see if this particular change will impact performance (either positively or negatively). I will test this with a GK.

+1 -1

3 comments

1 changed file

acdlite

pr closed time in 9 days

PR opened facebook/react

[www] Disable Scheduler `timeout` w/ dynamic flag

Before attempting to land an expiration times refactor, I want to see if this particular change will impact performance (either positively or negatively). I will test this with a GK.

+1 -1

0 comment

1 changed file

pr created time in 9 days

create barnchacdlite/react

branch : timeout-dynamic-flag

created branch time in 9 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;++    if (!isSafeToReadFromSource) {+      // Preserve the pending update if the current priority doesn't include it.+      currentlyRenderingFiber.expirationTime = pendingExpirationTime;+      markUnprocessedUpdateTime(pendingExpirationTime);+    }+  } else {+    // If there are no pending updates, we should still check the work-in-progress version.+    // It's possible this source (or the part we are reading from) has just not yet been subscribed to.+    const lastReadVersion = getWorkInProgressVersion(source, isPrimaryRenderer);+    if (lastReadVersion === null) {+      // This is the only case where we need to actually update the version number.+      // A changed version number for a new source will throw and restart the render,+      // at which point the work-in-progress version Map will be reinitialized.+      // Once a source has been subscribed to, we use expiration time to determine safety.+      setWorkInProgressVersion(source, version, isPrimaryRenderer);

Yeah sorry, by "mount" I mean either a new hook instance or whenever the source or config changes.

bvaughn

comment created time in 13 days

pull request commentfacebook/react

useMutableSource hook

The lint build error is legit. I looked at the ReactFabric-dev artifact and found this:

// ReactFabric-dev.js:11656
useMutableSource: function(source, config) {
  currentHookNameInDev = "useMutableSource";
  mountHookTypesDev();
  return Snapshot > config;
}

Looks like some sort of build error related to Flow types?

Phew that we have the lint build task! Also an argument for why we should be running more of our tests against the bundles (and therefore moving away from .internal test files).

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 let renderExpirationTime: ExpirationTime = NoWork; // the work-in-progress hook. let currentlyRenderingFiber: Fiber = (null: any); +// The work-in-progress root fiber.+// Used by mutable source hook to detect tears from pending updates.+let currentlyRenderingRoot: FiberRoot = (null: any);

Pending a decision on this comment (which I'm still thinking about), I need to access the current root.

To clarify, you'd have to access the root in that proposal, too. It would be root.lastPendingMutationTime.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function useResponder(responder, props): ReactEventResponderListener<any, any> {   }; } +function useMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  resolveCurrentlyRenderingComponent();+  const getSnapshot = config.getSnapshot;+  return getSnapshot(source._source);

I can’t think of a server use case that isn’t actually Suspense/Flight. So never mind.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

+/**+ * Copyright (c) Facebook, Inc. and its affiliates.+ *+ * This source code is licensed under the MIT license found in the+ * LICENSE file in the root directory of this source tree.+ *+ * @flow+ */++import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime';+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';++// Mutable source version can be anything (e.g. number, string, immutable data structure)+// so long as it changes every time any part of the source changes.+export type Version = $NonMaybeType<mixed>;++// Tracks expiration time for all mutable sources with pending updates.+// Used to determine if a source is safe to read during updates.+// If there are no entries in this map for a given source,+// or if the current render’s expiration time is ≤ this value,+// it is safe to read from the source without tearing.+export type MutableSourcePendingUpdateMap = Map<+  MutableSource<any>,+  ExpirationTime,+>;++type MutableSourceConfig = {|+  getVersion: () => Version,+|};++export type MutableSource<Source: $NonMaybeType<mixed>> = {|+  _source: Source,++  _getVersion: () => Version,++  // Tracks the version of this source at the time it was most recently read.+  // Used to determine if a source is safe to read from before it has been subscribed to.+  // Version number is only used during mount,+  // since the mechanism for determining safety after subscription is expiration time.+  //+  // As a workaround to support multiple concurrent renderers,+  // we categorize some renderers as primary and others as secondary.+  // We only expect there to be two concurrent renderers at most:+  // React Native (primary) and Fabric (secondary);+  // React DOM (primary) and React ART (secondary).+  // Secondary renderers store their context values on separate fields.+  // We use the same approach for Context.+  _workInProgressVersionPrimary: null | Version,+  _workInProgressVersionSecondary: null | Version,+|};++export type MutableSourceHookConfig<Source: $NonMaybeType<mixed>, Snapshot> = {|+  getSnapshot: (source: Source) => Snapshot,+  subscribe: (source: Source, callback: Function) => () => void,+|};++// Work in progress version numbers only apply to a single render,+// and should be reset before starting a new render.+// This tracks which mutable sources need to be reset after a render.+let workInProgressPrimarySources: Array<MutableSource<any>> = [];+let workInProgressSecondarySources: Array<MutableSource<any>> = [];++export function createMutableSource<Source: $NonMaybeType<mixed>>(+  source: Source,+  config: MutableSourceConfig,+): MutableSource<Source> {+  return {+    _getVersion: config.getVersion,+    _source: source,+    _workInProgressVersionPrimary: null,+    _workInProgressVersionSecondary: null,+  };+}++export function clearPendingUpdates(+  root: FiberRoot,+  expirationTime: ExpirationTime,+): void {+  // Remove pending mutable source entries that we've completed processing.+  root.mutableSourcePendingUpdateMap.forEach(+    (pendingExpirationTime, mutableSource) => {+      if (pendingExpirationTime <= expirationTime) {+        root.mutableSourcePendingUpdateMap.delete(mutableSource);+      }+    },+  );+}++export function resetWorkInProgressVersions(isPrimaryRenderer: boolean): void {+  if (isPrimaryRenderer) {+    workInProgressPrimarySources.forEach(mutableSource => {

Nit: This is an array, not a Map or a Set, so let's use a for loop like we do elsewhere.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function prepareFreshStack(root, expirationTime) {   workInProgressRootNextUnprocessedUpdateTime = NoWork;   workInProgressRootHasPendingPing = false; +  resetMutableSourceWorkInProgressVersions(isPrimaryRenderer);

Let's move this to completeWork and unwindWork of HostRoot so it gets cleared when exiting the render phase, instead of when starting the next render phase that happens to follow that. (This is akin to popping context providers off the stack.)

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;++    if (!isSafeToReadFromSource) {+      // Preserve the pending update if the current priority doesn't include it.+      currentlyRenderingFiber.expirationTime = pendingExpirationTime;+      markUnprocessedUpdateTime(pendingExpirationTime);+    }+  } else {+    // If there are no pending updates, we should still check the work-in-progress version.+    // It's possible this source (or the part we are reading from) has just not yet been subscribed to.+    const lastReadVersion = getWorkInProgressVersion(source, isPrimaryRenderer);+    if (lastReadVersion === null) {+      // This is the only case where we need to actually update the version number.+      // A changed version number for a new source will throw and restart the render,+      // at which point the work-in-progress version Map will be reinitialized.+      // Once a source has been subscribed to, we use expiration time to determine safety.+      setWorkInProgressVersion(source, version, isPrimaryRenderer);++      isSafeToReadFromSource = true;+    } else {+      isSafeToReadFromSource = lastReadVersion === version;+    }+  }++  let prevMemoizedState = ((hook.memoizedState: any): ?MutableSourceState<+    Source,+    Snapshot,+  >);+  let snapshot = ((null: any): Snapshot);++  if (isSafeToReadFromSource) {+    snapshot = getSnapshot(source._source);+  } else {+    // Since we can't read safely, can we reuse a previous snapshot?+    if (+      prevMemoizedState != null &&+      prevMemoizedState.config === config &&+      prevMemoizedState.source === source+    ) {+      snapshot = prevMemoizedState.snapshot;+    } else {+      // We can't read from the source and we can't reuse the snapshot,+      // so the only option left is to throw and restart the render.+      // This error should cause React to restart work on this root,+      // and flush all pending udpates (including any pending source updates).+      // This error won't be user-visible unless we throw again during re-render,+      // but we should not do this since the retry-render will be synchronous.+      // It would be a React error if this message was ever user visible.+      throw Error(+        'Cannot read from mutable source during the current render without tearing. ' ++          'This is a bug in React. Please file an issue.',+      );+    }+  }++  const prevSource =+    prevMemoizedState != null ? prevMemoizedState.source : null;+  const destroy =+    prevMemoizedState != null ? prevMemoizedState.destroy : undefined;++  const memoizedState: MutableSourceState<Source, Snapshot> = {+    config,+    destroy,+    snapshot,+    source,+  };++  hook.memoizedState = memoizedState;++  if (prevSource !== source) {+    const fiber = currentlyRenderingFiber;+    const root = currentlyRenderingRoot;++    const create = () => {+      const scheduleUpdate = () => {+        const alreadyScheduledExpirationTime = root.mutableSourcePendingUpdateMap.get(+          source,+        );++        // If an update is already scheduled for this source, re-use the same priority.+        if (alreadyScheduledExpirationTime !== undefined) {

This check means that if you update at one priority, then again at a different priority, the first priority always wins. We should support multiple concurrent priorities, which is solved by switching to a state queue. (The test case I described in my other comment would cover this.)

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function useResponder(responder, props): ReactEventResponderListener<any, any> {   }; } +function useMutableSource<Source, Snapshot>(+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  resolveCurrentlyRenderingComponent();+  const getSnapshot = config.getSnapshot;+  return getSnapshot(source._source);

Hmm even the existing server renderer supports streaming. So you technically could get a concurrent mutation. Not sure if it's worth supporting here. Probably at least warrants a TODO.

I assume the Fizz server renderer will work using a per-request cache? cc: @sebmarkbage

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

+/**+ * Copyright (c) Facebook, Inc. and its affiliates.+ *+ * This source code is licensed under the MIT license found in the+ * LICENSE file in the root directory of this source tree.+ *+ * @flow+ */++import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime';+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';++// Mutable source version can be anything (e.g. number, string, immutable data structure)+// so long as it changes every time any part of the source changes.+export type Version = $NonMaybeType<mixed>;++// Tracks expiration time for all mutable sources with pending updates.+// Used to determine if a source is safe to read during updates.+// If there are no entries in this map for a given source,+// or if the current render’s expiration time is ≤ this value,+// it is safe to read from the source without tearing.+export type MutableSourcePendingUpdateMap = Map<+  MutableSource<any>,+  ExpirationTime,+>;++type MutableSourceConfig = {|+  getVersion: () => Version,+|};++export type MutableSource<Source: $NonMaybeType<mixed>> = {|+  _source: Source,++  _getVersion: () => Version,++  // Tracks the version of this source at the time it was most recently read.+  // Used to determine if a source is safe to read from before it has been subscribed to.+  // Version number is only used during mount,+  // since the mechanism for determining safety after subscription is expiration time.+  //+  // As a workaround to support multiple concurrent renderers,+  // we categorize some renderers as primary and others as secondary.+  // We only expect there to be two concurrent renderers at most:+  // React Native (primary) and Fabric (secondary);+  // React DOM (primary) and React ART (secondary).+  // Secondary renderers store their context values on separate fields.+  // We use the same approach for Context.+  _workInProgressVersionPrimary: null | Version,+  _workInProgressVersionSecondary: null | Version,+|};++export type MutableSourceHookConfig<Source: $NonMaybeType<mixed>, Snapshot> = {|+  getSnapshot: (source: Source) => Snapshot,+  subscribe: (source: Source, callback: Function) => () => void,+|};++// Work in progress version numbers only apply to a single render,+// and should be reset before starting a new render.+// This tracks which mutable sources need to be reset after a render.+let workInProgressPrimarySources: Array<MutableSource<any>> = [];+let workInProgressSecondarySources: Array<MutableSource<any>> = [];++export function createMutableSource<Source: $NonMaybeType<mixed>>(+  source: Source,+  config: MutableSourceConfig,+): MutableSource<Source> {+  return {+    _getVersion: config.getVersion,+    _source: source,+    _workInProgressVersionPrimary: null,+    _workInProgressVersionSecondary: null,+  };+}++export function clearPendingUpdates(+  root: FiberRoot,+  expirationTime: ExpirationTime,+): void {+  // Remove pending mutable source entries that we've completed processing.+  root.mutableSourcePendingUpdateMap.forEach(+    (pendingExpirationTime, mutableSource) => {+      if (pendingExpirationTime <= expirationTime) {+        root.mutableSourcePendingUpdateMap.delete(mutableSource);+      }+    },+  );+}++export function resetWorkInProgressVersions(isPrimaryRenderer: boolean): void {+  if (isPrimaryRenderer) {+    workInProgressPrimarySources.forEach(mutableSource => {+      mutableSource._workInProgressVersionPrimary = null;+    });+    workInProgressPrimarySources.length = 0;+  } else {+    workInProgressSecondarySources.forEach(mutableSource => {+      mutableSource._workInProgressVersionSecondary = null;+    });+    workInProgressSecondarySources.length = 0;+  }+}++export function getPendingExpirationTime(+  root: FiberRoot,+  source: MutableSource<any>,+): ExpirationTime | null {+  const expirationTime = root.mutableSourcePendingUpdateMap.get(source);+  return expirationTime !== undefined ? expirationTime : null;+}++export function getWorkInProgressVersion(+  mutableSource: MutableSource<any>,+  isPrimaryRenderer: boolean,+): null | Version {+  if (isPrimaryRenderer) {+    return mutableSource._workInProgressVersionPrimary;+  } else {+    return mutableSource._workInProgressVersionSecondary;+  }+}++export function setWorkInProgressVersion(+  mutableSource: MutableSource<any>,+  version: Version,+  isPrimaryRenderer: boolean,+): void {+  if (isPrimaryRenderer) {+    mutableSource._workInProgressVersionPrimary = version;

In the context implementation, we fire a warning if we detect multiple primary renderers.

https://github.com/facebook/react/blob/2d6be757df86177ca8590bf7c361d6c910640895/packages/react-reconciler/src/ReactFiberNewContext.js#L86-L97

We should probably do the same here. Although I think it will need to go in getWorkInProgressVersion instead of set.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;++    if (!isSafeToReadFromSource) {+      // Preserve the pending update if the current priority doesn't include it.+      currentlyRenderingFiber.expirationTime = pendingExpirationTime;+      markUnprocessedUpdateTime(pendingExpirationTime);+    }+  } else {+    // If there are no pending updates, we should still check the work-in-progress version.+    // It's possible this source (or the part we are reading from) has just not yet been subscribed to.+    const lastReadVersion = getWorkInProgressVersion(source, isPrimaryRenderer);+    if (lastReadVersion === null) {+      // This is the only case where we need to actually update the version number.+      // A changed version number for a new source will throw and restart the render,+      // at which point the work-in-progress version Map will be reinitialized.+      // Once a source has been subscribed to, we use expiration time to determine safety.+      setWorkInProgressVersion(source, version, isPrimaryRenderer);

If we use a state queue (see my later comment), we only need to populate the work-in-progress version if this is a new mount, because updates don't rely on mutable state: they read from a queue, the result of which is guaranteed to be consistent with the rest of the tree for the expiration time at which we're rendering.

I'll write more details below.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;++    if (!isSafeToReadFromSource) {+      // Preserve the pending update if the current priority doesn't include it.+      currentlyRenderingFiber.expirationTime = pendingExpirationTime;+      markUnprocessedUpdateTime(pendingExpirationTime);+    }+  } else {+    // If there are no pending updates, we should still check the work-in-progress version.+    // It's possible this source (or the part we are reading from) has just not yet been subscribed to.+    const lastReadVersion = getWorkInProgressVersion(source, isPrimaryRenderer);+    if (lastReadVersion === null) {+      // This is the only case where we need to actually update the version number.+      // A changed version number for a new source will throw and restart the render,+      // at which point the work-in-progress version Map will be reinitialized.+      // Once a source has been subscribed to, we use expiration time to determine safety.+      setWorkInProgressVersion(source, version, isPrimaryRenderer);++      isSafeToReadFromSource = true;+    } else {+      isSafeToReadFromSource = lastReadVersion === version;+    }+  }++  let prevMemoizedState = ((hook.memoizedState: any): ?MutableSourceState<+    Source,+    Snapshot,+  >);+  let snapshot = ((null: any): Snapshot);++  if (isSafeToReadFromSource) {+    snapshot = getSnapshot(source._source);+  } else {+    // Since we can't read safely, can we reuse a previous snapshot?+    if (+      prevMemoizedState != null &&+      prevMemoizedState.config === config &&+      prevMemoizedState.source === source+    ) {+      snapshot = prevMemoizedState.snapshot;+    } else {+      // We can't read from the source and we can't reuse the snapshot,+      // so the only option left is to throw and restart the render.+      // This error should cause React to restart work on this root,+      // and flush all pending udpates (including any pending source updates).+      // This error won't be user-visible unless we throw again during re-render,+      // but we should not do this since the retry-render will be synchronous.+      // It would be a React error if this message was ever user visible.+      throw Error(

Our error minification infra doesn't support Error constructors yet, unfortunately. Let's use invariant 😭

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function commitHookEffectListMount(tag: number, finishedWork: Fiber) {       if ((effect.tag & tag) === tag) {         // Mount         const create = effect.create;-        effect.destroy = create();+        if (typeof create === 'function') {

I guess the type change here reveals that we are currently retaining create functions for longer than necessary 😆

And I guess it also reveals that we don't warn if create is not a function 😯The current behavior would throw.

So this check you've added is actually a semantic change, because if someone were to pass a non-function to useEffect, it would gracefully no-op instead of throwing.

We should warn in the render phase if it's not a function. And then change this check to create === undefined instead of a typeof.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function commitRootImpl(root, renderPriorityLevel) {     nestedUpdateCount = 0;   } +  // Clear any pending updates that were just processed.+  clearPendingMutableSourceUpdates(root, expirationTime);

Nit: Let's do this inside markRootFinishedAtTime since they do conceptually similar things.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

+/**+ * Copyright (c) Facebook, Inc. and its affiliates.+ *+ * This source code is licensed under the MIT license found in the+ * LICENSE file in the root directory of this source tree.+ *+ * @flow+ */++import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime';+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';++// Mutable source version can be anything (e.g. number, string, immutable data structure)+// so long as it changes every time any part of the source changes.+export type Version = $NonMaybeType<mixed>;++// Tracks expiration time for all mutable sources with pending updates.+// Used to determine if a source is safe to read during updates.+// If there are no entries in this map for a given source,+// or if the current render’s expiration time is ≤ this value,+// it is safe to read from the source without tearing.+export type MutableSourcePendingUpdateMap = Map<+  MutableSource<any>,+  ExpirationTime,+>;++type MutableSourceConfig = {|+  getVersion: () => Version,+|};++export type MutableSource<Source: $NonMaybeType<mixed>> = {|+  _source: Source,++  _getVersion: () => Version,++  // Tracks the version of this source at the time it was most recently read.+  // Used to determine if a source is safe to read from before it has been subscribed to.+  // Version number is only used during mount,+  // since the mechanism for determining safety after subscription is expiration time.+  //+  // As a workaround to support multiple concurrent renderers,+  // we categorize some renderers as primary and others as secondary.+  // We only expect there to be two concurrent renderers at most:+  // React Native (primary) and Fabric (secondary);+  // React DOM (primary) and React ART (secondary).+  // Secondary renderers store their context values on separate fields.+  // We use the same approach for Context.+  _workInProgressVersionPrimary: null | Version,+  _workInProgressVersionSecondary: null | Version,+|};++export type MutableSourceHookConfig<Source: $NonMaybeType<mixed>, Snapshot> = {|+  getSnapshot: (source: Source) => Snapshot,+  subscribe: (source: Source, callback: Function) => () => void,+|};++// Work in progress version numbers only apply to a single render,+// and should be reset before starting a new render.+// This tracks which mutable sources need to be reset after a render.+let workInProgressPrimarySources: Array<MutableSource<any>> = [];+let workInProgressSecondarySources: Array<MutableSource<any>> = [];++export function createMutableSource<Source: $NonMaybeType<mixed>>(+  source: Source,+  config: MutableSourceConfig,+): MutableSource<Source> {+  return {+    _getVersion: config.getVersion,+    _source: source,+    _workInProgressVersionPrimary: null,+    _workInProgressVersionSecondary: null,+  };+}++export function clearPendingUpdates(+  root: FiberRoot,+  expirationTime: ExpirationTime,+): void {+  // Remove pending mutable source entries that we've completed processing.+  root.mutableSourcePendingUpdateMap.forEach(+    (pendingExpirationTime, mutableSource) => {+      if (pendingExpirationTime <= expirationTime) {+        root.mutableSourcePendingUpdateMap.delete(mutableSource);+      }+    },+  );+}++export function resetWorkInProgressVersions(isPrimaryRenderer: boolean): void {

If you move this to the renderer then you can also remove the isPrimaryRenderer argument by importing it directly from the host config (that's the main reason I noticed where this file was located).

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 type BaseFiberRootProperties = {|   // render again   lastPingedTime: ExpirationTime,   lastExpiredTime: ExpirationTime,+  // Used by useMutableSource hook to avoid tearing within this root+  // when external, mutable sources are read from during render.+  mutableSourcePendingUpdateMap: MutableSourcePendingUpdateMap,

We should consider making this a single expiration time for all mutable sources: lastPendingMutationTime. To check if a render is inclusive of a pending mutation, you check if it's equal to or less than the last pending mutation time. That matches how we handle other types of pending work, including pings and discrete updates.

The trade off is that we might deopt slightly more often. Specifically when 1) there are mutations on multiple sources at the same time 2) at different priorities 3) and a new useMutationHook mounts during a higher priority render. (It only affects the mount case because a render that only includes updates should never de-opt; see my other comment.) So I'd argue it's not worth the added complexity.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

+/**+ * Copyright (c) Facebook, Inc. and its affiliates.+ *+ * This source code is licensed under the MIT license found in the+ * LICENSE file in the root directory of this source tree.+ *+ * @flow+ */++import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime';+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';++// Mutable source version can be anything (e.g. number, string, immutable data structure)+// so long as it changes every time any part of the source changes.+export type Version = $NonMaybeType<mixed>;++// Tracks expiration time for all mutable sources with pending updates.+// Used to determine if a source is safe to read during updates.+// If there are no entries in this map for a given source,+// or if the current render’s expiration time is ≤ this value,+// it is safe to read from the source without tearing.+export type MutableSourcePendingUpdateMap = Map<+  MutableSource<any>,+  ExpirationTime,+>;++type MutableSourceConfig = {|+  getVersion: () => Version,+|};++export type MutableSource<Source: $NonMaybeType<mixed>> = {|+  _source: Source,++  _getVersion: () => Version,++  // Tracks the version of this source at the time it was most recently read.+  // Used to determine if a source is safe to read from before it has been subscribed to.+  // Version number is only used during mount,+  // since the mechanism for determining safety after subscription is expiration time.+  //+  // As a workaround to support multiple concurrent renderers,+  // we categorize some renderers as primary and others as secondary.+  // We only expect there to be two concurrent renderers at most:+  // React Native (primary) and Fabric (secondary);+  // React DOM (primary) and React ART (secondary).+  // Secondary renderers store their context values on separate fields.+  // We use the same approach for Context.+  _workInProgressVersionPrimary: null | Version,+  _workInProgressVersionSecondary: null | Version,+|};++export type MutableSourceHookConfig<Source: $NonMaybeType<mixed>, Snapshot> = {|+  getSnapshot: (source: Source) => Snapshot,+  subscribe: (source: Source, callback: Function) => () => void,+|};++// Work in progress version numbers only apply to a single render,+// and should be reset before starting a new render.+// This tracks which mutable sources need to be reset after a render.+let workInProgressPrimarySources: Array<MutableSource<any>> = [];+let workInProgressSecondarySources: Array<MutableSource<any>> = [];++export function createMutableSource<Source: $NonMaybeType<mixed>>(+  source: Source,+  config: MutableSourceConfig,+): MutableSource<Source> {+  return {+    _getVersion: config.getVersion,+    _source: source,+    _workInProgressVersionPrimary: null,+    _workInProgressVersionSecondary: null,+  };+}++export function clearPendingUpdates(+  root: FiberRoot,+  expirationTime: ExpirationTime,+): void {+  // Remove pending mutable source entries that we've completed processing.+  root.mutableSourcePendingUpdateMap.forEach(+    (pendingExpirationTime, mutableSource) => {+      if (pendingExpirationTime <= expirationTime) {+        root.mutableSourcePendingUpdateMap.delete(mutableSource);+      }+    },+  );+}++export function resetWorkInProgressVersions(isPrimaryRenderer: boolean): void {+  if (isPrimaryRenderer) {

In the context implementation, we fire a warning if we detect multiple primary renderers.

https://github.com/facebook/react/blob/2d6be757df86177ca8590bf7c361d6c910640895/packages/react-reconciler/src/ReactFiberNewContext.js#L86-L97

We should probably do the same here.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;++    if (!isSafeToReadFromSource) {+      // Preserve the pending update if the current priority doesn't include it.+      currentlyRenderingFiber.expirationTime = pendingExpirationTime;+      markUnprocessedUpdateTime(pendingExpirationTime);+    }+  } else {+    // If there are no pending updates, we should still check the work-in-progress version.+    // It's possible this source (or the part we are reading from) has just not yet been subscribed to.+    const lastReadVersion = getWorkInProgressVersion(source, isPrimaryRenderer);+    if (lastReadVersion === null) {+      // This is the only case where we need to actually update the version number.+      // A changed version number for a new source will throw and restart the render,+      // at which point the work-in-progress version Map will be reinitialized.+      // Once a source has been subscribed to, we use expiration time to determine safety.+      setWorkInProgressVersion(source, version, isPrimaryRenderer);++      isSafeToReadFromSource = true;+    } else {+      isSafeToReadFromSource = lastReadVersion === version;+    }+  }++  let prevMemoizedState = ((hook.memoizedState: any): ?MutableSourceState<+    Source,+    Snapshot,+  >);+  let snapshot = ((null: any): Snapshot);++  if (isSafeToReadFromSource) {+    snapshot = getSnapshot(source._source);+  } else {+    // Since we can't read safely, can we reuse a previous snapshot?+    if (+      prevMemoizedState != null &&+      prevMemoizedState.config === config &&+      prevMemoizedState.source === source+    ) {+      snapshot = prevMemoizedState.snapshot;+    } else {+      // We can't read from the source and we can't reuse the snapshot,+      // so the only option left is to throw and restart the render.+      // This error should cause React to restart work on this root,+      // and flush all pending udpates (including any pending source updates).+      // This error won't be user-visible unless we throw again during re-render,+      // but we should not do this since the retry-render will be synchronous.+      // It would be a React error if this message was ever user visible.+      throw Error(+        'Cannot read from mutable source during the current render without tearing. ' ++          'This is a bug in React. Please file an issue.',+      );+    }+  }++  const prevSource =+    prevMemoizedState != null ? prevMemoizedState.source : null;+  const destroy =+    prevMemoizedState != null ? prevMemoizedState.destroy : undefined;++  const memoizedState: MutableSourceState<Source, Snapshot> = {+    config,+    destroy,+    snapshot,+    source,+  };++  hook.memoizedState = memoizedState;++  if (prevSource !== source) {

What if the config changes?

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;

Related: if the source version matches the work-in-progress version, then you don't need to check if the current render is inclusive of the pending mutation updates. Because that check would only fail if there was a mutation since the work-in-progress version was originally set, in which case the the version would have been bumped.

In other words, you only have to check for pending mutation updates if there's not already a work-in-progress version, i.e. if this is the first mutable read of this source during this render. For subsequent reads, the version alone is sufficient to tell if there was a mutation.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(+      currentTime,+      currentlyRenderingFiber,+      suspenseConfig,+    );++    isSafeToReadFromSource =+      pendingExpirationTime === NoWork ||+      pendingExpirationTime >= expirationTime;++    if (!isSafeToReadFromSource) {+      // Preserve the pending update if the current priority doesn't include it.+      currentlyRenderingFiber.expirationTime = pendingExpirationTime;+      markUnprocessedUpdateTime(pendingExpirationTime);+    }+  } else {+    // If there are no pending updates, we should still check the work-in-progress version.+    // It's possible this source (or the part we are reading from) has just not yet been subscribed to.+    const lastReadVersion = getWorkInProgressVersion(source, isPrimaryRenderer);+    if (lastReadVersion === null) {+      // This is the only case where we need to actually update the version number.+      // A changed version number for a new source will throw and restart the render,+      // at which point the work-in-progress version Map will be reinitialized.+      // Once a source has been subscribed to, we use expiration time to determine safety.+      setWorkInProgressVersion(source, version, isPrimaryRenderer);++      isSafeToReadFromSource = true;+    } else {+      isSafeToReadFromSource = lastReadVersion === version;+    }+  }++  let prevMemoizedState = ((hook.memoizedState: any): ?MutableSourceState<+    Source,+    Snapshot,+  >);+  let snapshot = ((null: any): Snapshot);++  if (isSafeToReadFromSource) {+    snapshot = getSnapshot(source._source);+  } else {+    // Since we can't read safely, can we reuse a previous snapshot?+    if (+      prevMemoizedState != null &&+      prevMemoizedState.config === config &&+      prevMemoizedState.source === source+    ) {+      snapshot = prevMemoizedState.snapshot;+    } else {+      // We can't read from the source and we can't reuse the snapshot,+      // so the only option left is to throw and restart the render.+      // This error should cause React to restart work on this root,+      // and flush all pending udpates (including any pending source updates).+      // This error won't be user-visible unless we throw again during re-render,+      // but we should not do this since the retry-render will be synchronous.+      // It would be a React error if this message was ever user visible.+      throw Error(+        'Cannot read from mutable source during the current render without tearing. ' ++          'This is a bug in React. Please file an issue.',+      );+    }+  }++  const prevSource =+    prevMemoizedState != null ? prevMemoizedState.source : null;+  const destroy =+    prevMemoizedState != null ? prevMemoizedState.destroy : undefined;++  const memoizedState: MutableSourceState<Source, Snapshot> = {+    config,+    destroy,+    snapshot,+    source,+  };++  hook.memoizedState = memoizedState;++  if (prevSource !== source) {+    const fiber = currentlyRenderingFiber;+    const root = currentlyRenderingRoot;++    const create = () => {+      const scheduleUpdate = () => {+        const alreadyScheduledExpirationTime = root.mutableSourcePendingUpdateMap.get(+          source,+        );++        // If an update is already scheduled for this source, re-use the same priority.+        if (alreadyScheduledExpirationTime !== undefined) {+          scheduleWork(fiber, alreadyScheduledExpirationTime);+        } else {+          const currentTime = requestCurrentTimeForUpdate();+          const suspenseConfig = requestCurrentSuspenseConfig();+          const expirationTime = computeExpirationForFiber(+            currentTime,+            fiber,+            suspenseConfig,+          );+          scheduleWork(fiber, expirationTime);++          // Make sure reads during future renders will know there's a pending update.+          // This will prevent a higher priority update from reading a newer version of the source,+          // and causing a tear between that render and previous renders.+          root.mutableSourcePendingUpdateMap.set(source, expirationTime);+        }+      };++      // Was the source mutated between when we rendered and when we're subscribing?+      // If so, we also need to schedule an update.+      const maybeNewSnapshot = getSnapshot(source._source);

You could compare to the version instead. Should usually be faster than getting a snapshot, which might allocate if there is selector-ish stuff in there.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 let renderExpirationTime: ExpirationTime = NoWork; // the work-in-progress hook. let currentlyRenderingFiber: Fiber = (null: any); +// The work-in-progress root fiber.+// Used by mutable source hook to detect tears from pending updates.+let currentlyRenderingRoot: FiberRoot = (null: any);

Since this is only used by useMutableSource it might be better to import a function getWorkInProgressRoot from the work loop. Closure will inline it. Then you don't have to set up a new variable to shadow the other one, or thread it through all the other functions in beginWork.

Although as I type this out, I don't really know why this shouldn't also apply to currentRenderingFiber, which is the same as the work loop's workInProgress. Except that workInProgress is accessed more frequently by hooks, and we don't have to go out of our way to thread it through beginWork because it's already used by those functions.

I could have the wrong intuition here over so I'll defer to @sebmarkbage's judgment

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

+/**+ * Copyright (c) Facebook, Inc. and its affiliates.+ *+ * This source code is licensed under the MIT license found in the+ * LICENSE file in the root directory of this source tree.+ *+ * @flow+ */++import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime';+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';++// Mutable source version can be anything (e.g. number, string, immutable data structure)+// so long as it changes every time any part of the source changes.+export type Version = $NonMaybeType<mixed>;++// Tracks expiration time for all mutable sources with pending updates.+// Used to determine if a source is safe to read during updates.+// If there are no entries in this map for a given source,+// or if the current render’s expiration time is ≤ this value,+// it is safe to read from the source without tearing.+export type MutableSourcePendingUpdateMap = Map<+  MutableSource<any>,+  ExpirationTime,+>;++type MutableSourceConfig = {|+  getVersion: () => Version,+|};++export type MutableSource<Source: $NonMaybeType<mixed>> = {|+  _source: Source,++  _getVersion: () => Version,++  // Tracks the version of this source at the time it was most recently read.+  // Used to determine if a source is safe to read from before it has been subscribed to.+  // Version number is only used during mount,+  // since the mechanism for determining safety after subscription is expiration time.+  //+  // As a workaround to support multiple concurrent renderers,+  // we categorize some renderers as primary and others as secondary.+  // We only expect there to be two concurrent renderers at most:+  // React Native (primary) and Fabric (secondary);+  // React DOM (primary) and React ART (secondary).+  // Secondary renderers store their context values on separate fields.+  // We use the same approach for Context.+  _workInProgressVersionPrimary: null | Version,+  _workInProgressVersionSecondary: null | Version,+|};++export type MutableSourceHookConfig<Source: $NonMaybeType<mixed>, Snapshot> = {|+  getSnapshot: (source: Source) => Snapshot,+  subscribe: (source: Source, callback: Function) => () => void,+|};++// Work in progress version numbers only apply to a single render,+// and should be reset before starting a new render.+// This tracks which mutable sources need to be reset after a render.+let workInProgressPrimarySources: Array<MutableSource<any>> = [];+let workInProgressSecondarySources: Array<MutableSource<any>> = [];++export function createMutableSource<Source: $NonMaybeType<mixed>>(

This file will get inlined into both the isomorphic package and the renderers. Rollup and Closure are probably smart enough to DCE the unused functions, but it doesn't hurt to be explicit about the package boundaries. Let's move createMutableSource to the isomorphic package, since it's only used by that, and the other functions to react-reconciler, i.e. react-reconciler/src/ReactFiberMutableSource.js.

The type can stay here, or it can go into shared/ReactTypes.

(That's a lot of files, I know 😆 We do the same thing for createContext and lazy.)

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 export function computeExpirationForFiber(   return expirationTime; } -export function scheduleUpdateOnFiber(-  fiber: Fiber,-  expirationTime: ExpirationTime,-) {+function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTime) {

Do you mind you removing scheduleWork instead? 😆 I meant to rename all the callsites to the more specific name during the last refactor.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

useMutableSource hook

 function rerenderReducer<S, I, A>(   return [newState, dispatch]; } +type MutableSourceState<Source, Snapshot> = {|+  config: MutableSourceHookConfig<Source, Snapshot>,+  destroy?: () => void,+  snapshot: Snapshot,+  source: MutableSource<any>,+|};++function useMutableSourceImpl<Source, Snapshot>(+  hook: Hook,+  source: MutableSource<Source>,+  config: MutableSourceHookConfig<Source, Snapshot>,+): Snapshot {+  const {getSnapshot, subscribe} = config;+  const version = source._getVersion();+  const pendingExpirationTime = getPendingExpirationTime(+    currentlyRenderingRoot,+    source,+  );++  // Is it safe to read from this source during the current render?+  let isSafeToReadFromSource = false;++  if (pendingExpirationTime !== null) {+    // If the source has pending updates, we can use the current render's expiration+    // time to determine if it's safe to read again from the source.+    const currentTime = requestCurrentTimeForUpdate();+    const suspenseConfig = requestCurrentSuspenseConfig();+    const expirationTime = computeExpirationForFiber(

computeExpirationForFiber is used for scheduling a new update. (A better name is maybe computeUpdateExpirationTimeForFiber or requestUpdateExpirationTime.)

I think you mean to compare to renderExpirationTime, which is the expiration time of the current render pass.

bvaughn

comment created time in 13 days

Pull request review commentfacebook/react

Refactor unmountHostComponents to recursively unmount

 function commitPlacement(finishedWork: Fiber): void { }  function unmountHostComponents(-  finishedRoot,-  current,-  renderPriorityLevel,+  finishedRoot: FiberRoot,+  current: Fiber,+  renderPriorityLevel: ReactPriorityLevel, ): void {   // We only have the top Fiber that was deleted but we need to recurse down its   // children to find all the terminal nodes.-  let node: Fiber = current;--  // Each iteration, currentParent is populated with node's host parent if not-  // currentParentIsValid.-  let currentParentIsValid = false;-   // Note: these two variables *must* always be updated together.   let currentParent;   let currentParentIsContainer; -  while (true) {-    if (!currentParentIsValid) {-      let parent = node.return;-      findParent: while (true) {-        invariant(-          parent !== null,-          'Expected to find a host parent. This error is likely caused by ' +-            'a bug in React. Please file an issue.',-        );-        const parentStateNode = parent.stateNode;-        switch (parent.tag) {-          case HostComponent:-            currentParent = parentStateNode;-            currentParentIsContainer = false;-            break findParent;-          case HostRoot:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case HostPortal:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case FundamentalComponent:-            if (enableFundamentalAPI) {-              currentParent = parentStateNode.instance;-              currentParentIsContainer = false;-            }+  let parent = current.return;+  findParent: while (true) {+    invariant(+      parent !== null,+      'Expected to find a host parent. This error is likely caused by ' ++        'a bug in React. Please file an issue.',+    );+    const parentStateNode = parent.stateNode;+    switch (parent.tag) {+      case HostComponent:+        currentParent = parentStateNode;+        currentParentIsContainer = false;+        break findParent;+      case HostRoot:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case HostPortal:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case FundamentalComponent:+        if (enableFundamentalAPI) {+          currentParent = parentStateNode.instance;+          currentParentIsContainer = false;         }-        parent = parent.return;-      }-      currentParentIsValid = true;     }+    parent = parent.return;+  }+  recursivelyUnmountHostNode(

Even the minified version? 😮

Ok the types were already like this so nbd

trueadm

comment created time in 13 days

Pull request review commentfacebook/react

Refactor unmountHostComponents to recursively unmount

 function commitPlacement(finishedWork: Fiber): void { }  function unmountHostComponents(-  finishedRoot,-  current,-  renderPriorityLevel,+  finishedRoot: FiberRoot,+  current: Fiber,+  renderPriorityLevel: ReactPriorityLevel, ): void {   // We only have the top Fiber that was deleted but we need to recurse down its   // children to find all the terminal nodes.-  let node: Fiber = current;--  // Each iteration, currentParent is populated with node's host parent if not-  // currentParentIsValid.-  let currentParentIsValid = false;-   // Note: these two variables *must* always be updated together.   let currentParent;   let currentParentIsContainer; -  while (true) {-    if (!currentParentIsValid) {-      let parent = node.return;-      findParent: while (true) {-        invariant(-          parent !== null,-          'Expected to find a host parent. This error is likely caused by ' +-            'a bug in React. Please file an issue.',-        );-        const parentStateNode = parent.stateNode;-        switch (parent.tag) {-          case HostComponent:-            currentParent = parentStateNode;-            currentParentIsContainer = false;-            break findParent;-          case HostRoot:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case HostPortal:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case FundamentalComponent:-            if (enableFundamentalAPI) {-              currentParent = parentStateNode.instance;-              currentParentIsContainer = false;-            }+  let parent = current.return;+  findParent: while (true) {+    invariant(+      parent !== null,+      'Expected to find a host parent. This error is likely caused by ' ++        'a bug in React. Please file an issue.',+    );+    const parentStateNode = parent.stateNode;+    switch (parent.tag) {+      case HostComponent:+        currentParent = parentStateNode;+        currentParentIsContainer = false;+        break findParent;+      case HostRoot:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case HostPortal:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case FundamentalComponent:+        if (enableFundamentalAPI) {+          currentParent = parentStateNode.instance;+          currentParentIsContainer = false;

I know this isn't new to this PR, but shouldn't this branch have a break statement?

trueadm

comment created time in 14 days

Pull request review commentfacebook/react

Refactor unmountHostComponents to recursively unmount

 function commitPlacement(finishedWork: Fiber): void { }  function unmountHostComponents(-  finishedRoot,-  current,-  renderPriorityLevel,+  finishedRoot: FiberRoot,+  current: Fiber,+  renderPriorityLevel: ReactPriorityLevel, ): void {   // We only have the top Fiber that was deleted but we need to recurse down its   // children to find all the terminal nodes.-  let node: Fiber = current;--  // Each iteration, currentParent is populated with node's host parent if not-  // currentParentIsValid.-  let currentParentIsValid = false;-   // Note: these two variables *must* always be updated together.   let currentParent;   let currentParentIsContainer; -  while (true) {-    if (!currentParentIsValid) {-      let parent = node.return;-      findParent: while (true) {-        invariant(-          parent !== null,-          'Expected to find a host parent. This error is likely caused by ' +-            'a bug in React. Please file an issue.',-        );-        const parentStateNode = parent.stateNode;-        switch (parent.tag) {-          case HostComponent:-            currentParent = parentStateNode;-            currentParentIsContainer = false;-            break findParent;-          case HostRoot:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case HostPortal:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case FundamentalComponent:-            if (enableFundamentalAPI) {-              currentParent = parentStateNode.instance;-              currentParentIsContainer = false;-            }+  let parent = current.return;+  findParent: while (true) {+    invariant(+      parent !== null,+      'Expected to find a host parent. This error is likely caused by ' ++        'a bug in React. Please file an issue.',+    );+    const parentStateNode = parent.stateNode;+    switch (parent.tag) {+      case HostComponent:+        currentParent = parentStateNode;+        currentParentIsContainer = false;+        break findParent;+      case HostRoot:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case HostPortal:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case FundamentalComponent:+        if (enableFundamentalAPI) {+          currentParent = parentStateNode.instance;+          currentParentIsContainer = false;         }-        parent = parent.return;-      }-      currentParentIsValid = true;     }+    parent = parent.return;+  }+  recursivelyUnmountHostNode(

You can move this call into each of the switch statement cases above.

trueadm

comment created time in 14 days

Pull request review commentfacebook/react

Refactor unmountHostComponents to recursively unmount

 function commitPlacement(finishedWork: Fiber): void { }  function unmountHostComponents(-  finishedRoot,-  current,-  renderPriorityLevel,+  finishedRoot: FiberRoot,+  current: Fiber,+  renderPriorityLevel: ReactPriorityLevel, ): void {   // We only have the top Fiber that was deleted but we need to recurse down its   // children to find all the terminal nodes.-  let node: Fiber = current;--  // Each iteration, currentParent is populated with node's host parent if not-  // currentParentIsValid.-  let currentParentIsValid = false;-   // Note: these two variables *must* always be updated together.   let currentParent;   let currentParentIsContainer; -  while (true) {-    if (!currentParentIsValid) {-      let parent = node.return;-      findParent: while (true) {-        invariant(-          parent !== null,-          'Expected to find a host parent. This error is likely caused by ' +-            'a bug in React. Please file an issue.',-        );-        const parentStateNode = parent.stateNode;-        switch (parent.tag) {-          case HostComponent:-            currentParent = parentStateNode;-            currentParentIsContainer = false;-            break findParent;-          case HostRoot:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case HostPortal:-            currentParent = parentStateNode.containerInfo;-            currentParentIsContainer = true;-            break findParent;-          case FundamentalComponent:-            if (enableFundamentalAPI) {-              currentParent = parentStateNode.instance;-              currentParentIsContainer = false;-            }+  let parent = current.return;+  findParent: while (true) {+    invariant(+      parent !== null,+      'Expected to find a host parent. This error is likely caused by ' ++        'a bug in React. Please file an issue.',+    );+    const parentStateNode = parent.stateNode;+    switch (parent.tag) {+      case HostComponent:+        currentParent = parentStateNode;+        currentParentIsContainer = false;+        break findParent;+      case HostRoot:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case HostPortal:+        currentParent = parentStateNode.containerInfo;+        currentParentIsContainer = true;+        break findParent;+      case FundamentalComponent:+        if (enableFundamentalAPI) {+          currentParent = parentStateNode.instance;+          currentParentIsContainer = false;         }-        parent = parent.return;-      }-      currentParentIsValid = true;     }+    parent = parent.return;+  }+  recursivelyUnmountHostNode(+    current,+    // These fields can never be uninitiated because of the above+    // loop and invariant+    ((currentParent: any): Instance),+    ((currentParentIsContainer: any): boolean),+    finishedRoot,+    renderPriorityLevel,+  );+} -    if (node.tag === HostComponent || node.tag === HostText) {-      commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);-      // After all the children have unmounted, it is now safe to remove the-      // node from the tree.-      if (currentParentIsContainer) {-        removeChildFromContainer(-          ((currentParent: any): Container),-          (node.stateNode: Instance | TextInstance),-        );-      } else {-        removeChild(-          ((currentParent: any): Instance),-          (node.stateNode: Instance | TextInstance),-        );-      }-      // Don't visit children because we already visited them.-    } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {-      const fundamentalNode = node.stateNode.instance;-      commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);-      // After all the children have unmounted, it is now safe to remove the-      // node from the tree.-      if (currentParentIsContainer) {-        removeChildFromContainer(-          ((currentParent: any): Container),-          (fundamentalNode: Instance),-        );-      } else {-        removeChild(-          ((currentParent: any): Instance),-          (fundamentalNode: Instance),-        );-      }-    } else if (-      enableSuspenseServerRenderer &&-      node.tag === DehydratedFragment-    ) {-      if (enableSuspenseCallback) {-        const hydrationCallbacks = finishedRoot.hydrationCallbacks;-        if (hydrationCallbacks !== null) {-          const onDeleted = hydrationCallbacks.onDeleted;-          if (onDeleted) {-            onDeleted((node.stateNode: SuspenseInstance));-          }+function recursivelyUnmountHostNode(+  node: Fiber,+  currentParent: Instance,+  currentParentIsContainer: boolean,+  finishedRoot: FiberRoot,+  renderPriorityLevel: ReactPriorityLevel,+): void {+  const {stateNode, tag} = node;+  if (tag === HostComponent || tag === HostText) {+    commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);+    // After all the children have unmounted, it is now safe to remove the+    // node from the tree.+    if (currentParentIsContainer) {+      removeChildFromContainer(currentParent, stateNode);+    } else {+      removeChild(currentParent, stateNode);+    }+    // Don't visit children because we already visited them.+  } else if (enableFundamentalAPI && tag === FundamentalComponent) {+    const fundamentalNode = node.stateNode.instance;+    commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);+    // After all the children have unmounted, it is now safe to remove the+    // node from the tree.+    if (currentParentIsContainer) {+      removeChildFromContainer(currentParent, fundamentalNode);+    } else {+      removeChild(currentParent, fundamentalNode);+    }+  } else if (enableSuspenseServerRenderer && tag === DehydratedFragment) {+    if (enableSuspenseCallback) {+      const hydrationCallbacks = finishedRoot.hydrationCallbacks;+      if (hydrationCallbacks !== null) {+        const onDeleted = hydrationCallbacks.onDeleted;+        if (onDeleted) {+          onDeleted(stateNode);         }       }--      // Delete the dehydrated suspense boundary and all of its content.-      if (currentParentIsContainer) {-        clearSuspenseBoundaryFromContainer(-          ((currentParent: any): Container),-          (node.stateNode: SuspenseInstance),-        );-      } else {-        clearSuspenseBoundary(-          ((currentParent: any): Instance),-          (node.stateNode: SuspenseInstance),-        );-      }-    } else if (node.tag === HostPortal) {-      if (node.child !== null) {-        // When we go into a portal, it becomes the parent to remove from.-        // We will reassign it back when we pop the portal on the way up.-        currentParent = node.stateNode.containerInfo;-        currentParentIsContainer = true;-        // Visit children because portals might contain host components.-        node.child.return = node;

Remove this assignment is probably the riskiest part of the change. Since it's in the commit phase I don't think it will break anything but might want to wait to land this until after we've set up the GK in www to quickly revert to an older version of React.

Another option would be to include the assignment in the new, recursive implementation, too, behind its own feature flag.

Let's just make sure whoever does the next sync knows about this.

trueadm

comment created time in 14 days

Pull request review commentfacebook/react

Split recent passive effects changes into 2 flags

 export const enableTrustedTypesIntegration = false; // Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance export const enableNativeTargetAsInstance = false; +// Controls sequence of passive effect destroy and create functions.+// If this flag is off, destroy and create functions may be interleaved.+// When the falg is on, all destroy functions will be run (for all fibers)+// before any create functions are run, similar to how layout effects work.+// This flag provides a killswitch if that proves to break existing code somehow.+export const runAllPassiveEffectDestroysBeforeCreates = false;+ // Controls behavior of deferred effect destroy functions during unmount. // Previously these functions were run during commit (along with layout effects). // Ideally we should delay these until after commit for performance reasons. // This flag provides a killswitch if that proves to break existing code somehow.+//+// WARNING Only enable this flag in conjunction with runAllPassiveEffectDestroysBeforeCreates.

Alternative, in the code, you could also check for both booleans before you enable the "defer unmounts" feature.

Just want to make it harder for someone to accidentally toggle the wrong combination of GKs.

bvaughn

comment created time in 14 days

Pull request review commentfacebook/react

Re-throw errors thrown by the renderer at the root in the complete phase

 function handleError(root, thrownValue) {         // boundary.         workInProgressRootExitStatus = RootFatalErrored;         workInProgressRootFatalError = thrownValue;+        workInProgress = null;

In other words, it falls into an infinite loop because workInProgress is stuck on the the host root fiber forever. Usually workInProgress gets advanced during either beingWork or completeUnitOfWork but since it keeps erroring that never happens.

acdlite

comment created time in 14 days

Pull request review commentfacebook/react

Re-throw errors thrown by the renderer at the root in the complete phase

 function handleError(root, thrownValue) {         // boundary.         workInProgressRootExitStatus = RootFatalErrored;         workInProgressRootFatalError = thrownValue;+        workInProgress = null;

This adds a new test case and fixes a bug where React would keep retrying the root because the workInProgress pointer was not advanced to the next fiber. (Which in this case is null, since it's the root.)

acdlite

comment created time in 14 days

Pull request review commentfacebook/react

Split recent passive effects changes into 2 flags

 export const enableTrustedTypesIntegration = false; // Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance export const enableNativeTargetAsInstance = false; +// Controls sequence of passive effect destroy and create functions.+// If this flag is off, destroy and create functions may be interleaved.+// When the falg is on, all destroy functions will be run (for all fibers)+// before any create functions are run, similar to how layout effects work.+// This flag provides a killswitch if that proves to break existing code somehow.+export const runAllPassiveEffectDestroysBeforeCreates = false;+ // Controls behavior of deferred effect destroy functions during unmount. // Previously these functions were run during commit (along with layout effects). // Ideally we should delay these until after commit for performance reasons. // This flag provides a killswitch if that proves to break existing code somehow.+//+// WARNING Only enable this flag in conjunction with runAllPassiveEffectDestroysBeforeCreates.

Instead of two booleans you could use an enum, to prevent the invalid combination

bvaughn

comment created time in 14 days

push eventacdlite/react

Andrew Clark

commit sha 2299d1df0c24667ac1f4f24eca48db7f34690595

Re-throw errors thrown by the renderer at the root React treats errors thrown at the root as a fatal because there's no parent component that can capture it. (This is distinct from an "uncaught error" that isn't wrapped in an error boundary, because in that case we can fall back to deleting the whole tree -- not great, but at least the error is contained to a single root, and React is left in a consistent state.) It turns out we didn't have a test case for this path. The only way it can happen is if the renderer's host config throws. We had similar test cases for host components, but none for the host root. This adds a new test case and fixes a bug where React would keep retrying the root because the `workInProgress` pointer was not advanced to the next fiber. (Which in this case is `null`, since it's the root.) We could consider in the future trying to gracefully exit from certain types of root errors without leaving React in an inconsistent state. For example, we should be able to gracefully exit from errors thrown in the begin phase. For now, I'm treating it like an internal invariant and immediately exiting.

view details

push time in 15 days

push eventacdlite/react

Andrew Clark

commit sha a5a03661dc722dd3c007b055f02832a3ec5f3ef3

Re-throw root errors thrown in complete phase React treats errors thrown at the root as a fatal because there's no parent component that can capture it. (This is distinct from an "uncaught error" that isn't wrapped in an error boundary, because in that case we can fall back to deleting the whole tree -- not great, but at least the error is contained to a single root, and React is left in a consistent state.) The only way it can happen is if the renderer's host config throws. It turns out we had a test case for this when the error is thrown in the begin phase, but not the complete phase. This adds a test case for the complete phase and fixes a bug where React would keep retrying the root because the `workInProgress` pointer was not advanced to the next fiber. (Which in this case is `null`, since it's the root.) We could consider in the future trying to gracefully exit from certain types of root errors without leaving React in an inconsistent state. For now, I'm treating it like an internal invariant and immediately exiting.

view details

push time in 15 days

PR opened facebook/react

Reviewers
Re-throw errors thrown by the renderer at the root

React treats errors thrown at the root as a fatal because there's no parent component that can capture it. (This is distinct from an "uncaught error" that isn't wrapped in an error boundary, because in that case we can fall back to deleting the whole tree -- not great, but at least the error is contained to a single root, and React is left in a consistent state.)

It turns out we didn't have a test case for this path. The only way it can happen is if the renderer's host config throws. We had similar test cases for host components, but none for the host root.

This adds a new test case and fixes a bug where React would keep retrying the root because the workInProgress pointer was not advanced to the next fiber. (Which in this case is null, since it's the root.)

We could consider in the future trying to gracefully exit from certain types of root errors without leaving React in an inconsistent state. For example, we should be able to gracefully exit from errors thrown in the begin phase. For now, I'm treating it like an internal invariant and immediately exiting.

+20 -0

0 comment

3 changed files

pr created time in 15 days

pull request commentfacebook/react

Add Modern WWW build

Hmm I was thinking you'd use requireCond but I can see how that would be tedious to set up for every possible artifact.

gaearon

comment created time in 15 days

create barnchacdlite/react

branch : rootfatalerror

created branch time in 15 days

Pull request review commentfacebook/react

Bugfix: Expired partial tree infinite loops

 function workLoopSync() {   } } +function renderRootConcurrent(root, expirationTime) {

Yeah I only did it for symmetry purposes

acdlite

comment created time in 16 days

pull request commentfacebook/react

Add FB_WWW_MODERN bundle types

Might have been a misunderstanding. @sebmarkbage and I chatted offline. Let’s discuss at the meeting!

gaearon

comment created time in 16 days

pull request commentfacebook/react

Use testing builds for our own tests

I tried this and it failed because it kept referring to itself and failing the build.

You can move the default feature flags to their own file, shared/forks/ReactFeatureFlags.default. Then shared/ReactFeatureFlags can re-export that. And so can all the forks.

threepointone

comment created time in 19 days

Pull request review commentfacebook/react

Use testing builds for our own tests

 // It lets us determine whether we're running in Fire mode without making tests internal. const ReactFeatureFlags = require('../ReactFeatureFlags'); // Forbid writes because this wouldn't work with bundle tests.-module.exports = Object.freeze({...ReactFeatureFlags});+module.exports = Object.freeze({...ReactFeatureFlags, isTestEnvironment: true});

Don't need this anymore, right?

threepointone

comment created time in 19 days

Pull request review commentfacebook/react

Use testing builds for our own tests

     "README.md",     "build-info.json",     "index.js",+    "testing.js",

Putting this here means it will go into the next public release. If you can't get rid of it because the bundle tests rely on it, maybe prefix with unstable?

threepointone

comment created time in 19 days

Pull request review commentfacebook/react

Refactor commitPlacement to recursively insert nodes

 function commitPlacement(finishedWork: Fiber): void {   const before = getHostSibling(finishedWork);   // We only have the top Fiber that was inserted but we need to recurse down its   // children to find all the terminal nodes.-  let node: Fiber = finishedWork;-  while (true) {-    const isHost = node.tag === HostComponent || node.tag === HostText;-    if (isHost || (enableFundamentalAPI && node.tag === FundamentalComponent)) {-      const stateNode = isHost ? node.stateNode : node.stateNode.instance;-      if (before) {-        if (isContainer) {-          insertInContainerBefore(parent, stateNode, before);-        } else {-          insertBefore(parent, stateNode, before);-        }+  insertOrAppendPlacementNode(finishedWork, before, parent, isContainer);+}++function insertOrAppendPlacementNode(+  node: Fiber,+  before: ?Instance,+  parent: Instance,+  isContainer: boolean,

This boolean never changes, so you can fork this function: insertOrAppendPlacementNode and insertOrAppendPlacementNodeIntoContainer. Then the Flow types will be correct, too.

trueadm

comment created time in 20 days

pull request commentfacebook/react

Use testing builds for our own tests

You can reexport one from the other:

// shared/forks/ReactFeatureFlags.www.testing.js

export {
  flags,
  that,
  are,
  the,
  same
} from './ReactFeatureFlags.www';

export const differentFlag = 'forked';
threepointone

comment created time in 20 days

Pull request review commentfacebook/react

Use testing builds for our own tests

 function getPlugins(     bundleType === RN_FB_DEV ||     bundleType === RN_FB_PROD ||     bundleType === RN_FB_PROFILING;+  const isTestBuild = entry === 'react-dom/testing';

Shouldn't this also be true for React Noop and React Test Renderer?

threepointone

comment created time in 21 days

push eventacdlite/react

Andrew Clark

commit sha dc2812e43bca1ac6e9b16d5951f6fac4a3cd6c3d

Factor out render phase Splits the work loop and its surrounding enter/exit code into their own functions. Now we can do perform multiple render phase passes within a single call to performConcurrentWorkOnRoot or performSyncWorkOnRoot. This lets us get rid of the `didError` field.

view details

push time in 22 days

push eventacdlite/react

Sunil Pai

commit sha d84c539b31bc703fe6271965ce85344f50c8c207

fix sizebot - point correctly to circleci artifact (#17975) similar to #17972, this should fix sizebot not reporting stats right now

view details

Andrew Clark

commit sha 8f1a1138e3f03e5a57157535638c0ca7cb77a876

Bugfix: Expiring a partially completed tree (#17926) * Failing test: Expiring a partially completed tree We should not throw out a partially completed tree if it expires in the middle of rendering. We should finish the rest of the tree without yielding, then finish any remaining expired levels in a single batch. * Check if there's a partial tree before restarting If a partial render expires, we should stay in the concurrent path (performConcurrentWorkOnRoot); we'll stop yielding, but the rest of the behavior remains the same. We will only revert to the sync path (performSyncWorkOnRoot) when starting on a new level. This approach prevents partially completed concurrent work from being discarded. * New test: retry after error during expired render

view details

Andrew Clark

commit sha 4599c929977e78c702ce832e3b25d429b8ca0a6e

Regression: Expired partial tree infinite loops Adds regression tests that reproduce a scenario where a partially completed tree expired then fell into an infinite loop. The code change that exposed this bug made the assumption that if you call Scheduler's `shouldYield` from inside an expired task, Scheduler will always return `false`. But in fact, Scheduler sometimes returns `true` in that scenario, which is a bug. The reason it worked before is that once a task timed out, React would always switch to a synchronous work loop without checking `shouldYield`. My rationale for relying on `shouldYield` was to unify the code paths between a partially concurrent render (i.e. expires midway through) and a fully concurrent render, as opposed to a render that was synchronous the whole time. However, this bug indicates that we need a stronger guarantee within React for when tasks expire, given that the failure case is so catastrophic. Instead of relying on the result of a dynamic method call, we should use control flow to guarantee that the work is synchronously executed. (We should also fix the Scheduler bug so that `shouldYield` always returns false inside an expired task, but I'll address that separately.)

view details

Andrew Clark

commit sha 69b10d56adae317b05ab86d1e2a9cdf04047bdf9

Always switch to sync work loop when task expires Refactors the `didTimeout` check so that it always switches to the synchronous work loop, like it did before the regression. This breaks the error handling behavior that I added in 5f7361f (an error during a partially concurrent render should retry once, synchronously). I'll fix this next. I need to change that behavior, anyway, to support retries that occur as a result of `flushSync`.

view details

Andrew Clark

commit sha 19162302b0b0bb8d0668c36d5d65d44fe548e23c

Retry once after error even for sync renders Except in legacy mode. This is to support the `useOpaqueReference` hook, which uses an error to trigger a retry at lower priority.

view details

Andrew Clark

commit sha b074e6dc223a076c16d17df801c9d4f2d73aa693

Move partial tree check to performSyncWorkOnRoot

view details

push time in 22 days

pull request commentfacebook/react

Bugfix: Expired partial tree infinite loops

@sebmarkbage Based on your feedback I updated the PR in two commits. The first one moves the partial tree check to performSyncWorkOnRoot. That one is pretty simple by itself.

The second one removes didError by performing multiple render passes within the same call to performSyncWorkOnRoot or performConcurrentWorkOnRoot. To do this, I factored out the render phase. I think this refactor makes a lot of sense, though it's inherently riskier.

acdlite

comment created time in 22 days

push eventacdlite/react

Andrew Clark

commit sha acb551e01645053c4c1d503af0100fdb3bbd1358

Factor out render phase Splits the work loop and its surrounding enter/exit code into their own functions. Now we can do perform multiple render phase passes within a single call to performConcurrentWorkOnRoot or performSyncWorkOnRoot. This lets us get rid of the `didError` field.

view details

push time in 22 days

push eventacdlite/react

Andrew Clark

commit sha 010c09b1afdfa1be74f31ee2e389b19770e05c19

Move partial tree check to performSyncWorkOnRoot

view details

push time in 23 days

push eventacdlite/react

Farhad Yasir

commit sha d9a5170594486deeb767c243cc4b381e9e085c79

fix: check bigint in serializeToString and change it to string (#17931)

view details

Deniz Susman

commit sha 00745b053fa00424d0963a0b9d64e3fcb3f90298

Typo fix (#17946)

view details

Brian Vaughn

commit sha 9ad35905fae96036a130d9dec24b47132dfe4076

Add DevTools tests for copying complex values (#17948)

view details

Mark Huang

commit sha 08c1f79e1e13719ae2b79240bbd8f97178ddd791

Fix Cannot read property 'sub' of undefined when navigating to plain-text pages (#17848) Update various parts of DevTools to account for the fact that the global "hook" might be undefined if DevTools didn't inject it (due to the page's `contentType`) it (due to the page's `contentType`)

view details

Dominic Gannaway

commit sha 434770c3b4b94315c789234c27ed9dc2ec8a78ad

Add beforeRemoveInstance method to ReactNoop (#17959)

view details

Evyatar

commit sha 9944bf27fb75b330202f51f58a2ce5c0ea778bef

Add version property to ReactDOM (#15780)

view details

Brian Vaughn

commit sha 7df32c4c8c53d971e894c5e8d62a3cc908489b05

Flush `useEffect` clean up functions in the passive effects phase (#17925) * Flush useEffect clean up functions in the passive effects phase This is a change in behavior that may cause broken product code, so it has been added behind a killswitch (deferPassiveEffectCleanupDuringUnmount) * Avoid scheduling unnecessary callbacks for cleanup effects Updated enqueuePendingPassiveEffectDestroyFn() to check rootDoesHavePassiveEffects before scheduling a new callback. This way we'll only schedule (at most) one. * Updated newly added test for added clarity. * Cleaned up hooks effect tags We previously used separate Mount* and Unmount* tags to track hooks work for each phase (snapshot, mutation, layout, and passive). This was somewhat complicated to trace through and there were man tag types we never even used (e.g. UnmountLayout, MountMutation, UnmountSnapshot). In addition to this, it left passive and layout hooks looking the same after renders without changed dependencies, which meant we were unable to reliably defer passive effect destroy functions until after the commit phase. This commit reduces the effect tag types to only include Layout and Passive and differentiates between work and no-work with an HasEffect flag. * Disabled deferred passive effects flushing in OSS builds for now * Split up unmount and mount effects list traversal

view details

Sebastian Markbåge

commit sha ace9e8134c3080d86f20097d5ba3369e15a97a83

Simplify Continuous Hydration Targets (#17952) * Simplify Continuous Hydration Targets Let's use a constant priority for this. This helps us avoid restarting a render when switching targets and simplifies the model. The downside is that now we're not down-prioritizing the previous hover target. However, we think that's ok because it'll only do one level too much and then stop. * Add test meant to show why it's tricky to merge both hydration levels Having both levels co-exist works. However, if we deprioritize hydration using a single level, we might deprioritize the wrong thing. This adds a test that catches it if we ever try a naive deprioritization in the future. It also tests that we don't down-prioritize if we're changing the hover in the middle of doing continuous priority work.

view details

Sunil Pai

commit sha 3e9251d605692e6db6103e4fca9771ac30a62247

make testing builds for React/ReactDOM (#17915) This PR introduces adds `react/testing` and `react-dom/testing`. - changes infra to generate these builds - exports act on ReactDOM in these testing builds - uses the new test builds in fixtures/dom In the next PR - - I'll use the new builds for all our own tests - I'll replace usages of TestUtils.act with ReactDOM.act.

view details

Hassan Alam

commit sha 2078aa9a401aa91e97b42cdd36a6310888128ab2

Add dom fixture for autofilled form state (#17951)

view details

Alfredo Granja

commit sha 812277dab6b449596288825d59184dfc1acdf370

Fix onMouseEnter is fired on disabled buttons (#17675)

view details

Dan Abramov

commit sha d6e08fe0a806703241311b5331c21f29fdf4a139

Remove Suspense priority warning (#17971) * Remove Suspense priority warning * Fix tests

view details

Murat ÇATAL

commit sha cddde45806a8e37f534742a280fa18be79871d18

apply changes on editablevalue on blur feature implemented (#17062) * apply changes on editablevalue on blur feature implemented * Removed "Undo" button and unnecessary event.preventDefault() Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>

view details

Andrew Clark

commit sha 9dba218d933d66fba9a8ba23b90dbb852514da8b

[Mock Scheduler] Mimic browser's advanceTime (#17967) The mock Scheduler that we use in our tests has its own fake timer implementation. The `unstable_advanceTime` method advances the timeline. Currently, a call to `unstable_advanceTime` will also flush any pending expired work. But that's not how it works in the browser: when a timer fires, the corresponding task is added to the Scheduler queue. However, we will still wait until the next message event before flushing it. This commit changes `unstable_advanceTime` to more closely resemble the browser behavior, by removing the automatic flushing of expired work. ```js // Before this commit Scheduler.unstable_advanceTime(ms); // Equivalent behavior after this commit Scheduler.unstable_advanceTime(ms); Scheduler.unstable_flushExpired(); ``` The general principle is to prefer separate APIs for scheduling tasks and flushing them. This change does not affect any public APIs. `unstable_advanceTime` is only used by our own test suite. It is not used by `act`. However, we may need to update tests in www, like Relay's.

view details

Brian Vaughn

commit sha 562d2fbc497d5e8e6ae5934ae9f68b28cd613fab

Fix release scripts (#17972) Circle CI seems to have changed the reported artifact path which broke our scripts.

view details

Brian Vaughn

commit sha 613cbd3acef902c9737a8934f0950bf934c5f30e

Formatting fix (Prettier) to build script

view details

Andrew Clark

commit sha 0bf8996252eacced2b7165b8d9959177226f9e3c

Bugfix: Expiring a partially completed tree (#17926) * Failing test: Expiring a partially completed tree We should not throw out a partially completed tree if it expires in the middle of rendering. We should finish the rest of the tree without yielding, then finish any remaining expired levels in a single batch. * Check if there's a partial tree before restarting If a partial render expires, we should stay in the concurrent path (performConcurrentWorkOnRoot); we'll stop yielding, but the rest of the behavior remains the same. We will only revert to the sync path (performSyncWorkOnRoot) when starting on a new level. This approach prevents partially completed concurrent work from being discarded. * New test: retry after error during expired render

view details

Andrew Clark

commit sha 769281d48a5fa8bc54ff61e4c637fffbe352a82a

Regression: Expired partial tree infinite loops Adds regression tests that reproduce a scenario where a partially completed tree expired then fell into an infinite loop. The code change that exposed this bug made the assumption that if you call Scheduler's `shouldYield` from inside an expired task, Scheduler will always return `false`. But in fact, Scheduler sometimes returns `true` in that scenario, which is a bug. The reason it worked before is that once a task timed out, React would always switch to a synchronous work loop without checking `shouldYield`. My rationale for relying on `shouldYield` was to unify the code paths between a partially concurrent render (i.e. expires midway through) and a fully concurrent render, as opposed to a render that was synchronous the whole time. However, this bug indicates that we need a stronger guarantee within React for when tasks expire, given that the failure case is so catastrophic. Instead of relying on the result of a dynamic method call, we should use control flow to guarantee that the work is synchronously executed. (We should also fix the Scheduler bug so that `shouldYield` always returns false inside an expired task, but I'll address that separately.)

view details

Andrew Clark

commit sha b686f225614f83fb129ce383bf640a8756227323

Always switch to sync work loop when task expires Refactors the `didTimeout` check so that it always switches to the synchronous work loop, like it did before the regression. This breaks the error handling behavior that I added in 5f7361f (an error during a partially concurrent render should retry once, synchronously). I'll fix this next. I need to change that behavior, anyway, to support retries that occur as a result of `flushSync`.

view details

Andrew Clark

commit sha 613f75f868438c1e3125108951fca20084ad8b76

Retry once after error even for sync renders Except in legacy mode. This is to support the `useOpaqueReference` hook, which uses an error to trigger a retry at lower priority.

view details

push time in 23 days

push eventfacebook/react

Andrew Clark

commit sha 9dba218d933d66fba9a8ba23b90dbb852514da8b

[Mock Scheduler] Mimic browser's advanceTime (#17967) The mock Scheduler that we use in our tests has its own fake timer implementation. The `unstable_advanceTime` method advances the timeline. Currently, a call to `unstable_advanceTime` will also flush any pending expired work. But that's not how it works in the browser: when a timer fires, the corresponding task is added to the Scheduler queue. However, we will still wait until the next message event before flushing it. This commit changes `unstable_advanceTime` to more closely resemble the browser behavior, by removing the automatic flushing of expired work. ```js // Before this commit Scheduler.unstable_advanceTime(ms); // Equivalent behavior after this commit Scheduler.unstable_advanceTime(ms); Scheduler.unstable_flushExpired(); ``` The general principle is to prefer separate APIs for scheduling tasks and flushing them. This change does not affect any public APIs. `unstable_advanceTime` is only used by our own test suite. It is not used by `act`. However, we may need to update tests in www, like Relay's.

view details

push time in 23 days

PR merged facebook/react

Reviewers
[Mock Scheduler] Mimic browser's advanceTime CLA Signed React Core Team

The mock Scheduler that we use in our tests has its own fake timer implementation. The unstable_advanceTime method advances the timeline.

Currently, a call to unstable_advanceTime will also flush any pending expired work. But that's not how it works in the browser: when a timer fires, the corresponding task is added to the Scheduler queue. However, we will still wait until the next message event before flushing it.

This commit changes unstable_advanceTime to more closely resemble the browser behavior, by removing the automatic flushing of expired work.

// Before this commit
Scheduler.unstable_advanceTime(ms);

// Equivalent behavior after this commit
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_flushExpired();

The general principle is to prefer separate APIs for scheduling tasks and flushing them.

This change does not affect any public APIs. unstable_advanceTime is only used by our own test suite. It is not used by act.

However, we may need to update tests in www, like Relay's.

+14 -16

6 comments

5 changed files

acdlite

pr closed time in 23 days

pull request commentfacebook/react

[Mock Scheduler] Mimic browser's advanceTime

Yeah I think that API would make sense because it's a test matcher which discourages us from calling it inside another task.

acdlite

comment created time in 23 days

PR opened facebook/react

Reviewers
[Scheduler] shouldYield should always return false from inside an expired task

Based on #17967

The current behavior of shouldYield is unspecified except in cases that happen to be relied on by the React work loop's implementation. However, some of the unspecified cases are important and the consequences of assuming the wrong behavior are really bad.

For example, it's natural to assume that shouldYield returns false when called from within an expired task. But the current behavior usually does that, unless there's a higher priority task or a main thread task (i.e. at the end of the 5ms message loop interval).

This fixes shouldYield so that it always returns false inside an expired task, regardless of the other tasks in the queue.

+238 -26

0 comment

6 changed files

pr created time in 23 days

create barnchacdlite/react

branch : shouldyield-fixes

created branch time in 23 days

PR opened facebook/react

Reviewers
[Mock Scheduler] Mimic browser's advanceTime

The mock Scheduler that we use in our tests has its own fake timer implementation. The unstable_advanceTime method advances the timeline.

Currently, a call to unstable_advanceTime will also flush any pending expired work. But that's not how it works in the browser: when a timer fires, the corresponding task is added to the Scheduler queue. However, we will still wait until the next message event before flushing it.

This commit changes unstable_advanceTime to more closely resemble the browser behavior, by removing the automatic flushing of expired work.

// Before this commit
Scheduler.unstable_advanceTime(ms);

// Equivalent behavior after this commit
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_flushExpired();

The general principle is to prefer separate APIs for scheduling tasks and flushing them.

This change does not affect any public APIs. unstable_advanceTime is only used by our own test suite. It is not used by act.

However, we may need to update tests in www, like Relay's.

+14 -16

0 comment

5 changed files

pr created time in 24 days

pull request commentfacebook/react

Flush all passive destroy fns before calling create fns

I was thinking the way you could do this is by maintaining two arrays: one for destroy effects, another for create/init effects.

During the sync commit phase, traverse over the hook effect list and push to the appropriate array.

Then during commitPassiveEffects, you would iterate over the arrays without touching the fiber effect list at all.

The advantage of this approach is it's less total work because in the passive phase, you no longer have to visit the entire effect list. The disadvantage is it sometimes shifts more work to the layout phase, because you can't skip over a fiber that only has passive effects scheduled on it.

But the other advantage is that it saves on code size, which is why I'm leaning toward always pushing to an array. cc @sebmarkbage for input.

bvaughn

comment created time in 24 days

Pull request review commentfacebook/react

Flush all passive destroy fns before calling create fns

 function flushPassiveEffectsImpl() {       invokeGuardedCallback(null, destroy, null);     }     pendingUnmountedPassiveEffectDestroyFunctions.length = 0;-  } -  // Note: This currently assumes there are no passive effects on the root-  // fiber, because the root is not part of its own effect list. This could-  // change in the future.-  let effect = root.current.firstEffect;-  while (effect !== null) {-    if (__DEV__) {-      setCurrentDebugFiberInDEV(effect);-      invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);-      if (hasCaughtError()) {-        invariant(effect !== null, 'Should be working on an effect.');-        const error = clearCaughtError();-        captureCommitPhaseError(effect, error);+    // It's important that ALL pending passive effect destroy functions are called+    // before ANY passive effect create functions are called.+    // Otherwise effects in sibling components might interfere with each other.+    // e.g. a destroy function in one component may unintentionally override a ref+    // value set by a create function in another component.+    // Layout effects have the same constraint.++    // First pass: Destroy stale passive effects.+    //+    // Note: This currently assumes there are no passive effects on the root fiber+    // because the root is not part of its own effect list.+    // This could change in the future.+    let effect = root.current.firstEffect;+    let effectWithErrorDuringUnmount = null;+    while (effect !== null) {+      if (__DEV__) {+        setCurrentDebugFiberInDEV(effect);+        invokeGuardedCallback(+          null,+          commitPassiveHookUnmountEffects,+          null,+          effect,+        );+        if (hasCaughtError()) {+          effectWithErrorDuringUnmount = effect;+          invariant(effect !== null, 'Should be working on an effect.');+          const error = clearCaughtError();+          captureCommitPhaseError(effect, error);+        }+        resetCurrentDebugFiberInDEV();+      } else {+        try {+          commitPassiveHookUnmountEffects(effect);+        } catch (error) {+          effectWithErrorDuringUnmount = effect;+          invariant(effect !== null, 'Should be working on an effect.');+          captureCommitPhaseError(effect, error);+        }       }-      resetCurrentDebugFiberInDEV();-    } else {-      try {-        commitPassiveHookEffects(effect);-      } catch (error) {-        invariant(effect !== null, 'Should be working on an effect.');-        captureCommitPhaseError(effect, error);+      effect = effect.nextEffect;+    }++    // Second pass: Create new passive effects.+    //+    // Note: This currently assumes there are no passive effects on the root fiber+    // because the root is not part of its own effect list.+    // This could change in the future.+    effect = root.current.firstEffect;+    while (effect !== null) {+      // Don't run create effects for a Fiber that errored during destroy.+      // This check is in place to match previous behavior.+      // TODO: Rethink whether we want to carry this behavior forward.+      if (effectWithErrorDuringUnmount !== effect) {

If multiple fibers errored, this check only works for the last one. So it doesn't seem worth it.

bvaughn

comment created time in 24 days

create barnchacdlite/react

branch : scheduler-advance-time

created branch time in 24 days

more