Change:
Do you want to request a feature or report a bug? Bug What is the current behavior? is never set to true when calling within , but it does work properly when within a . Here's the correct behavior (accomplished via ): ! Here's the incorrect behavior (via ): ! Note the difference is that the opacity never changes to 0.4 (which is determined based on the state). What is the expected behavior? I expect them to both behave the same (at least as far as the user can observe). Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React? react react-dom
I guess startTransition is never intended to be used in effects. I didn't expect it even works. Looking at the doc again, Hmm, it doesn't clearly say so.
Ok but the use cases is interesting.
Do you think this is the same as
Yes, I believe that's the same issue.
Hello, I think I have a similar issue when using with . When returns a new value I would like to create a new resource. I tried to use for this, but I don't even have an idea where to put ...
you can see the isPending is if the is bigger than 5100 when calling in .
I'm seeing the same behavior as - setting to a value of 5200 or higher causes to fire reliably in the original CodeSandbox:
That is a bug we need to fix regardless.
Fixed by . Updated Sandbox:

Discussion:
import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; <del> import * as Scheduler from 'scheduler'; </del> import ReactSharedInternals from 'shared/ReactSharedInternals'; import {NoWork} from './ReactFiberExpirationTime';

import is from 'shared/objectIs'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; <del> import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration'; </del> <ins> import { UserBlockingPriority, NormalPriority, runWithPriority, getCurrentPriorityLevel, } from './SchedulerWithReactIntegration'; </ins> const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

const [prevValue, setValue] = mountState(value); mountEffect( () => { <del> Scheduler.unstable_next(() => { const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setValue(value); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } }); </del> <ins> const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setValue(value); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } </ins> }, [value, config], );

const [prevValue, setValue] = updateState(value); updateEffect( () => { <del> Scheduler.unstable_next(() => { const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setValue(value); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } }); </del> <ins> const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setValue(value); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } </ins> }, [value, config], ); return prevValue; } <ins> function startTransition(setPending, config, callback) { const priorityLevel = getCurrentPriorityLevel(); runWithPriority( priorityLevel < UserBlockingPriority ? UserBlockingPriority : priorityLevel, () => { setPending(true); }, ); runWithPriority( priorityLevel > NormalPriority ? NormalPriority : priorityLevel, () => { const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setPending(false); callback(); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } }, ); } </ins> function mountTransition( config: SuspenseConfig | void | null, ): [(() => void) => void, boolean] { const [isPending, setPending] = mountState(false); <del> const startTransition = mountCallback( callback => { setPending(true); Scheduler.unstable_next(() => { const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setPending(false); callback(); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } }); }, [config, isPending], ); return [startTransition, isPending]; </del> <ins> const start = mountCallback(startTransition.bind(null, setPending, config), [ setPending, config, ]); return [start, isPending]; </ins> } function updateTransition( config: SuspenseConfig | void | null, ): [(() => void) => void, boolean] { const [isPending, setPending] = updateState(false); <del> const startTransition = updateCallback( callback => { setPending(true); Scheduler.unstable_next(() => { const previousConfig = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; try { setPending(false); callback(); } finally { ReactCurrentBatchConfig.suspense = previousConfig; } }); }, [config, isPending], ); return [startTransition, isPending]; </del> <ins> const start = updateCallback(startTransition.bind(null, setPending, config), [ setPending, config, ]); return [start, isPending]; </ins> } function dispatchAction<S, A>(

<ins> /** * 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. * * @emails react-core * @jest-environment node */ 'use strict'; let ReactFeatureFlags; let React; let ReactNoop; let Scheduler; let Suspense; let useState; let useTransition; let act; describe('ReactHooksWithNoopRenderer', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableSchedulerTracing = true; ReactFeatureFlags.flushSuspenseFallbacksInTests = false; React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); useState = React.useState; useTransition = React.useTransition; Suspense = React.Suspense; act = ReactNoop.act; }); function Text(props) { Scheduler.unstable_yieldValue(props.text); return props.text; } function createAsyncText(text) { let resolved = false; let Component = function() { if (!resolved) { Scheduler.unstable_yieldValue('Suspend! [' + text + ']'); throw promise; } return <Text text={text} />; }; let promise = new Promise(resolve => { Component.resolve = function() { resolved = true; return resolve(); }; }); return Component; } it('isPending works even if called from outside an input event', async () => { const Async = createAsyncText('Async'); let start; function App() { const [show, setShow] = useState(false); const [startTransition, isPending] = useTransition(); start = () => startTransition(() => setShow(true)); return ( <Suspense fallback={<Text text="Loading..." />}> {isPending ? <Text text="Pending..." /> : null} {show ? <Async /> : <Text text="(empty)" />} </Suspense> ); } const root = ReactNoop.createRoot(); await act(async () => { root.render(<App />); }); expect(Scheduler).toHaveYielded(['(empty)']); expect(root).toMatchRenderedOutput('(empty)'); await act(async () => { start(); expect(Scheduler).toFlushAndYield([ 'Pending...', '(empty)', 'Suspend! [Async]', 'Loading...', ]); expect(root).toMatchRenderedOutput('Pending...(empty)'); await Async.resolve(); }); expect(Scheduler).toHaveYielded(['Async']); expect(root).toMatchRenderedOutput('Async'); }); }); </ins>