Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
efd4d049b1 [compiler] Improve setState-in-effects rule to account for ref-gated conditionals
Conditionally calling setState in an effect is sometimes necessary, but should generally follow the pattern of using a "previous vaue" ref to manually compare and ensure that the setState is idempotent. See fixture for an example.
2025-11-17 12:06:43 -08:00
18 changed files with 28 additions and 550 deletions

View File

@@ -44,21 +44,6 @@ function stripExtension(filename: string, extensions: Array<string>): string {
return filename;
}
/**
* Strip all extensions from a filename
* e.g., "foo.expect.md" -> "foo"
*/
function stripAllExtensions(filename: string): string {
let result = filename;
while (true) {
const extension = path.extname(result);
if (extension === '') {
return result;
}
result = path.basename(result, extension);
}
}
export async function readTestFilter(): Promise<TestFilter | null> {
if (!(await exists(FILTER_PATH))) {
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
@@ -126,25 +111,11 @@ async function readInputFixtures(
} else {
inputFiles = (
await Promise.all(
filter.paths.map(pattern => {
// If the pattern already has an extension other than .expect.md,
// search for the pattern directly. Otherwise, search for the
// pattern with the expected input extensions added.
// Eg
// `alias-while` => search for `alias-while{.js,.jsx,.ts,.tsx}`
// `alias-while.js` => search as-is
// `alias-while.expect.md` => search for `alias-while{.js,.jsx,.ts,.tsx}`
const basename = path.basename(pattern);
const basenameWithoutExt = stripAllExtensions(basename);
const hasExtension = basename !== basenameWithoutExt;
const globPattern =
hasExtension && !pattern.endsWith(SNAPSHOT_EXTENSION)
? pattern
: `${basenameWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
return glob.glob(globPattern, {
filter.paths.map(pattern =>
glob.glob(`${pattern}{${INPUT_EXTENSIONS.join(',')}}`, {
cwd: rootDir,
});
}),
}),
),
)
).flat();
}
@@ -179,13 +150,11 @@ async function readOutputFixtures(
} else {
outputFiles = (
await Promise.all(
filter.paths.map(pattern => {
// Strip all extensions and find matching .expect.md files
const basenameWithoutExt = stripAllExtensions(pattern);
return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
filter.paths.map(pattern =>
glob.glob(`${pattern}${SNAPSHOT_EXTENSION}`, {
cwd: rootDir,
});
}),
}),
),
)
).flat();
}

View File

@@ -35,7 +35,6 @@ type RunnerOptions = {
watch: boolean;
filter: boolean;
update: boolean;
pattern?: string;
};
const opts: RunnerOptions = yargs
@@ -63,15 +62,9 @@ const opts: RunnerOptions = yargs
'Only run fixtures which match the contents of testfilter.txt',
)
.default('filter', false)
.string('pattern')
.alias('p', 'pattern')
.describe(
'pattern',
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
)
.help('help')
.strict()
.parseSync(hideBin(process.argv)) as RunnerOptions;
.parseSync(hideBin(process.argv));
/**
* Do a test run and return the test results
@@ -178,13 +171,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
worker.getStderr().pipe(process.stderr);
worker.getStdout().pipe(process.stdout);
// If pattern is provided, force watch mode off and use pattern filter
const shouldWatch = opts.watch && opts.pattern == null;
if (opts.watch && opts.pattern != null) {
console.warn('NOTE: --watch is ignored when a --pattern is supplied');
}
if (shouldWatch) {
if (opts.watch) {
makeWatchRunner(state => onChange(worker, state), opts.filter);
if (opts.filter) {
/**
@@ -229,18 +216,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
try {
execSync('yarn build', {cwd: PROJECT_ROOT});
console.log('Built compiler successfully with tsup');
// Determine which filter to use
let testFilter: TestFilter | null = null;
if (opts.pattern) {
testFilter = {
debug: true,
paths: [opts.pattern],
};
} else if (opts.filter) {
testFilter = await readTestFilter();
}
const testFilter = opts.filter ? await readTestFilter() : null;
const results = await runFixtures(worker, testFilter, 0);
if (opts.update) {
update(results);

View File

@@ -14,7 +14,6 @@ import type {
Usable,
Thenable,
ReactDebugInfo,
ReactStore,
} from 'shared/ReactTypes';
import type {
ContextDependency,
@@ -482,22 +481,6 @@ function useSyncExternalStore<T>(
return value;
}
function useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
const value = selector(store._current);
hookLog.push({
displayName: null,
primitive: 'StoreWithSelector',
stackError: new Error(),
value,
debugInfo: null,
dispatcherHookName: 'StoreWithSelector',
});
return value;
}
function useTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
@@ -794,7 +777,6 @@ const Dispatcher: DispatcherType = {
useDeferredValue,
useTransition,
useSyncExternalStore,
useStoreWithSelector,
useId,
useHostTransitionStatus,
useFormState,

View File

@@ -14,7 +14,6 @@ import type {
Thenable,
RejectedThenable,
Awaited,
ReactStore,
} from 'shared/ReactTypes';
import type {
Fiber,
@@ -75,9 +74,6 @@ import {
isGestureRender,
GestureLane,
UpdateLanes,
includesOnlyTransitions,
includesTransitionLane,
SomeTransitionLane,
} from './ReactFiberLane';
import {
ContinuousEventPriority,
@@ -240,13 +236,6 @@ type StoreConsistencyCheck<T> = {
getSnapshot: () => T,
};
// TODO: Use something other than null
type StoreWithSelectorQueue<T> = {
syncEagerState: T | null,
transitionEagerState: T | null,
lanes: Lanes,
};
type EventFunctionPayload<Args, Return, F: (...Array<Args>) => Return> = {
ref: {
eventFn: F,
@@ -1819,141 +1808,6 @@ function updateSyncExternalStore<T>(
return nextSnapshot;
}
// Used as a placeholder to let us reuse updateReducerImpl for useStoreWithSelector.
function storeReducer<S, A>(state: S, action: A): S {
throw new Error(
'Should never be called. This is a bug in React. Please file an issue.',
);
}
function mountStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: S => T,
): T {
const fiber = currentlyRenderingFiber;
const isTransition = includesOnlyTransitions(renderLanes);
// TODO: If store._transition !== store._current, we need to
// enqueue an entangled fixup update to ensure consistency.
const initialState = selector(
isTransition ? store._transition : store._current,
);
const hook = mountWorkInProgressHook();
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, mixed> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: storeReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
mountEffect(createSubscription.bind(null, store, fiber, selector, queue), []);
return initialState;
}
function updateStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: S => T,
): T {
const hook = updateWorkInProgressHook();
const [state /* _dispatch */] = updateReducerImpl<T, mixed>(
hook,
((currentHook: any): Hook),
storeReducer,
);
const fiber = currentlyRenderingFiber;
const queue = hook.queue;
updateEffect(
createSubscription.bind(null, store, fiber, selector, queue),
[],
);
return state;
}
function createSubscription<S, T>(
store: ReactStore<S, mixed>,
fiber: Fiber,
selector: S => T,
queue: UpdateQueue<T, mixed>,
): () => void {
return store.subscribe(() => {
const lane = requestUpdateLane(fiber);
const isTransition = isTransitionLane(lane);
// Eagerly compute the new selected state
const newState = selector(
isTransition ? store._transition : store._current,
);
const update: Update<T, mixed> = {
lane,
revertLane: NoLane,
gesture: null,
action: null,
hasEagerState: true,
eagerState: newState,
next: (null: any),
};
const hasQueuedTransitionUpdate = includesTransitionLane(queue.lanes);
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
startUpdateTimerByLane(lane, 'store.dispatch()', fiber);
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
// The way React's update ordering works is that for each render we apply
// the updates for that render's lane, and skip over any updates that don't
// have sufficient priority. For normal reducer updates this means that we
// will:
// 1. Apply a sync update on top of the currently committed state.
// 2. Reattempt the pending transition update, this time with the sync
// update applied on top.
//
// However, we don't want each individual component's update to have to
// re-rerun the store's reducer in order to achieve this update reordering.
// Instead, if we know there is a pending transition update, we simply
// enqueue yet another transition update on top.
// The sync render will ignore this update but the subsequent transition render
// will apply it, giving us the desired final state.
//
// Ideally we could define a custom approach for store selector states, but
// for now this lets us reuse all of the very complex updateReducerImpl logic
// without changes.
if (hasQueuedTransitionUpdate && !isTransition) {
// TODO: We should determine the actual lane (lanes?) we need to use here.
const transitionLane = SomeTransitionLane;
const transitionState = selector(store._transition);
const transitionUpdate: Update<T, mixed> = {
lane: transitionLane,
revertLane: NoLane,
gesture: null,
action: null,
hasEagerState: true,
eagerState: transitionState,
next: (null: any),
};
const transitionRoot = enqueueConcurrentHookUpdate(
fiber,
queue,
transitionUpdate,
transitionLane,
);
if (transitionRoot !== null) {
startUpdateTimerByLane(transitionLane, 'store.dispatch()', fiber);
scheduleUpdateOnFiber(transitionRoot, fiber, transitionLane);
entangleTransitionUpdate(transitionRoot, queue, transitionLane);
}
}
});
}
function pushStoreConsistencyCheck<T>(
fiber: Fiber,
getSnapshot: () => T,
@@ -1993,7 +1847,7 @@ function updateStoreInstance<T>(
// Something may have been mutated in between render and commit. This could
// have been in an event that fired before the passive effects, or it could
// have been in a layout effect. In that case, we would have used the old
// snapshot and getSnapshot values to bail out. We need to check one more time.
// snapsho and getSnapshot values to bail out. We need to check one more time.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
// We intentionally don't log update times and stacks here because this
@@ -3978,7 +3832,7 @@ function enqueueRenderPhaseUpdate<S, A>(
// TODO: Move to ReactFiberConcurrentUpdates?
function entangleTransitionUpdate<S, A>(
root: FiberRoot,
queue: UpdateQueue<S, A> | StoreWithSelectorQueue<T>,
queue: UpdateQueue<S, A>,
lane: Lane,
): void {
if (isTransitionLane(lane)) {
@@ -4025,7 +3879,6 @@ export const ContextOnlyDispatcher: Dispatcher = {
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useStoreWithSelector: throwInvalidHookError,
useId: throwInvalidHookError,
useHostTransitionStatus: throwInvalidHookError,
useFormState: throwInvalidHookError,
@@ -4056,7 +3909,6 @@ const HooksDispatcherOnMount: Dispatcher = {
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useSyncExternalStore: mountSyncExternalStore,
useStoreWithSelector: mountStoreWithSelector,
useId: mountId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: mountActionState,
@@ -4087,7 +3939,6 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useSyncExternalStore: updateSyncExternalStore,
useStoreWithSelector: updateStoreWithSelector,
useId: updateId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: updateActionState,
@@ -4118,7 +3969,6 @@ const HooksDispatcherOnRerender: Dispatcher = {
useDeferredValue: rerenderDeferredValue,
useTransition: rerenderTransition,
useSyncExternalStore: updateSyncExternalStore,
useStoreWithSelector: updateStoreWithSelector,
useId: updateId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: rerenderActionState,
@@ -4280,14 +4130,6 @@ if (__DEV__) {
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
mountHookTypesDev();
return mountStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
mountHookTypesDev();
@@ -4455,14 +4297,6 @@ if (__DEV__) {
updateHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
updateHookTypesDev();
return mountStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
@@ -4630,14 +4464,6 @@ if (__DEV__) {
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
updateHookTypesDev();
return updateStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
@@ -4805,14 +4631,6 @@ if (__DEV__) {
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
updateHookTypesDev();
return updateStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
@@ -4998,15 +4816,6 @@ if (__DEV__) {
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
warnInvalidHookAccess();
mountHookTypesDev();
return mountStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
@@ -5199,15 +5008,6 @@ if (__DEV__) {
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
warnInvalidHookAccess();
updateHookTypesDev();
return updateStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
@@ -5400,15 +5200,6 @@ if (__DEV__) {
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
currentHookNameInDev = 'useStoreWithSelector';
warnInvalidHookAccess();
updateHookTypesDev();
return updateStoreWithSelector(store, selector);
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();

View File

@@ -17,7 +17,6 @@ import type {
Awaited,
ReactComponentInfo,
ReactDebugInfo,
ReactStore,
} from 'shared/ReactTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType';
import type {WorkTag} from './ReactWorkTags';
@@ -59,7 +58,6 @@ export type HookType =
| 'useDeferredValue'
| 'useTransition'
| 'useSyncExternalStore'
| 'useStoreWithSelector'
| 'useId'
| 'useCacheRefresh'
| 'useOptimistic'
@@ -439,10 +437,6 @@ export type Dispatcher = {
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T,
useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T,
useId(): string,
useCacheRefresh: () => <T>(?() => T, ?T) => void,
useMemoCache: (size: number) => Array<any>,

View File

@@ -1,14 +0,0 @@
baseQueue contains a mixture of updates at different priorities
`hook.queue` is stable. We could create that on mount, and use that to stash eager states.
`hook.memoizedState` is updated during update to be the newly computed state, and
is compared with the previous state to determine if an update should be reported.
## Plan
1. Create an object with both a sync update and transition update.
2. Use that as the hook queue
3. Close over that object in the subscription
4. On update, eagerly compute the new state and write it to the correct queue property.
5. During the render phase, check the current lane, if it's a transition, read from the transition eager state, otherwise read from the sync eager state.
6. Check if this is different than the previous memoized state, and if so, mark a pending update and update the memoized state.
7. Return the memoized state.

View File

@@ -1,147 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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
*/
'use strict';
let useStoreWithSelector;
let React;
let ReactNoop;
let Scheduler;
let act;
let createStore;
let startTransition;
let waitFor;
let assertLog;
describe('useStoreWithSelector', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
createStore = React.createStore;
useStoreWithSelector = React.useStoreWithSelector;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
act = require('internal-test-utils').act;
});
it('useStoreWithSelector', async () => {
function counterReducer(
count: number,
action: {type: 'increment' | 'decrement'},
): number {
switch (action.type) {
case 'increment':
return count + 1;
case 'decrement':
return count - 1;
default:
return count;
}
}
const store = createStore(counterReducer, 2);
function App() {
const value = useStoreWithSelector(store, identify);
Scheduler.log(value);
return <>{value}</>;
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
await waitFor([2]);
});
expect(root).toMatchRenderedOutput('2');
await act(async () => {
startTransition(() => {
store.dispatch({type: 'increment'});
});
});
assertLog([3]);
expect(root).toMatchRenderedOutput('3');
await act(async () => {
startTransition(() => {
store.dispatch({type: 'increment'});
});
});
assertLog([4]);
expect(root).toMatchRenderedOutput('4');
});
it('rebasing', async () => {
function counterReducer(
count: number,
action: {type: 'increment' | 'decrement'},
): number {
switch (action.type) {
case 'increment':
return count + 1;
case 'double':
return count * 2;
default:
return count;
}
}
const store = createStore(counterReducer, 2);
function App() {
const value = useStoreWithSelector(store, identify);
Scheduler.log(value);
return <>{value}</>;
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
await waitFor([2]);
});
expect(root).toMatchRenderedOutput('2');
let resolve;
await act(async () => {
await startTransition(async () => {
store.dispatch({type: 'increment'});
await new Promise(r => (resolve = r));
});
});
assertLog([]);
expect(root).toMatchRenderedOutput('2');
await act(async () => {
store.dispatch({type: 'double'});
});
assertLog([4]);
expect(root).toMatchRenderedOutput('4');
await act(async () => {
resolve();
});
assertLog([6]);
expect(root).toMatchRenderedOutput('6');
});
});
function identify<T>(x: T): T {
return x;
}

View File

@@ -16,7 +16,6 @@ import type {
Usable,
ReactCustomFormAction,
Awaited,
ReactStore,
} from 'shared/ReactTypes';
import type {ResumableState} from './ReactFizzConfig';
@@ -565,14 +564,6 @@ function useSyncExternalStore<T>(
return getServerSnapshot();
}
function useStoreWithSelector<S, T>(
store: ReactStore<S, mixed>,
selector: (state: S) => T,
): T {
resolveCurrentlyRenderingComponent();
return selector(store._current);
}
function useDeferredValue<T>(value: T, initialValue?: T): T {
resolveCurrentlyRenderingComponent();
return initialValue !== undefined ? initialValue : value;
@@ -836,7 +827,6 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
useId,
// Subscriptions are not setup in a server environment.
useSyncExternalStore,
useStoreWithSelector,
useOptimistic,
useActionState,
useFormState: useActionState,
@@ -861,7 +851,6 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
useDeferredValue: clientHookNotSupported,
useTransition: clientHookNotSupported,
useSyncExternalStore: clientHookNotSupported,
useStoreWithSelector: clientHookNotSupported,
useId,
useHostTransitionStatus,
useFormState: useActionState,

View File

@@ -86,7 +86,6 @@ export const HooksDispatcher: Dispatcher = {
useDeferredValue: (unsupportedHook: any),
useTransition: (unsupportedHook: any),
useSyncExternalStore: (unsupportedHook: any),
useStoreWithSelector: (unsupportedHook: any),
useId,
useHostTransitionStatus: (unsupportedHook: any),
useFormState: (unsupportedHook: any),

View File

@@ -21,7 +21,6 @@ export {
createContext,
createElement,
createRef,
createStore,
use,
forwardRef,
isValidElement,
@@ -53,7 +52,6 @@ export {
useRef,
useState,
useSyncExternalStore,
useStoreWithSelector,
useTransition,
useActionState,
version,

View File

@@ -21,7 +21,6 @@ export {
createContext,
createElement,
createRef,
createStore,
use,
forwardRef,
isValidElement,
@@ -54,7 +53,6 @@ export {
useRef,
useState,
useSyncExternalStore,
useStoreWithSelector,
useTransition,
useActionState,
version,

View File

@@ -35,7 +35,6 @@ export {
createContext,
createElement,
createRef,
createStore,
use,
forwardRef,
isValidElement,
@@ -66,7 +65,6 @@ export {
useMemo,
useOptimistic,
useSyncExternalStore,
useStoreWithSelector,
useReducer,
useRef,
useState,

View File

@@ -34,7 +34,6 @@ import {lazy} from './ReactLazy';
import {forwardRef} from './ReactForwardRef';
import {memo} from './ReactMemo';
import {cache, cacheSignal} from './ReactCacheClient';
import {createStore} from './ReactStore';
import {
getCacheForType,
useCallback,
@@ -47,7 +46,6 @@ import {
useLayoutEffect,
useMemo,
useSyncExternalStore,
useStoreWithSelector,
useReducer,
useRef,
useState,
@@ -85,7 +83,6 @@ export {
memo,
cache,
cacheSignal,
createStore,
useCallback,
useContext,
useEffect,
@@ -98,7 +95,6 @@ export {
useOptimistic,
useActionState,
useSyncExternalStore,
useStoreWithSelector,
useReducer,
useRef,
useState,

View File

@@ -13,7 +13,6 @@ import type {
StartTransitionOptions,
Usable,
Awaited,
ReactStore,
} from 'shared/ReactTypes';
import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols';
@@ -199,14 +198,6 @@ export function useSyncExternalStore<T>(
);
}
export function useStoreWithSelector<T, S>(
store: ReactStore<S, mixed>,
selector: S => T,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useStoreWithSelector(store, selector);
}
export function useCacheRefresh(): <T>(?() => T, ?T) => void {
const dispatcher = resolveDispatcher();
// $FlowFixMe[not-a-function] This is unstable, thus optional

View File

@@ -1,47 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {ReactStore} from 'shared/ReactTypes';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
export function createStore<S, A>(
reducer: (S, A) => S,
initialValue: S,
): ReactStore<S, A> {
const subscriptions = new Set<() => void>();
const self = {
_current: initialValue,
_transition: initialValue,
_reducer: reducer,
dispatch(action: A) {
if (ReactSharedInternals.T !== null) {
// We are in a transition, update the transition state
self._transition = reducer(self._transition, action);
} else if (is(self._current, self._transition)) {
// We are updating sync and no transition is in progress, update both
self._current = self._transition = reducer(self._transition, action);
} else {
// We are updating sync, but a transition is in progress. Implement
// React's update reordering semantics.
self._transition = reducer(self._transition, action);
self._current = reducer(self._current, action);
}
subscriptions.forEach(callback => callback());
},
subscribe(callback: () => void): () => void {
subscriptions.add(callback);
return () => {
subscriptions.delete(callback);
};
},
};
return self;
}

View File

@@ -381,11 +381,3 @@ export type ProfilerProps = {
) => void,
children?: ReactNodeList,
};
export type ReactStore<S, A> = {
_current: S,
_transition: S,
_reducer: (S, A) => S,
subscribe: (callback: () => void) => () => void,
dispatch: (action: A) => void,
};

View File

@@ -1 +1,15 @@
export default '19.3.0-canary-ea4899e1-20251117';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// TODO: this is special because it gets imported during build.
//
// It exists as a placeholder so that DevTools can support work tag changes between releases.
// When we next publish a release, update the matching TODO in backend/renderer.js
// TODO: This module is used both by the release scripts and to expose a version
// at runtime. We should instead inject the version number as part of the build
// process, and use the ReactVersions.js module as the single source of truth.
export default '19.3.0';

View File

@@ -24,7 +24,6 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"flow.pathToFlow": "${workspaceFolder}/node_modules/.bin/flow",
"flow.showUncovered": false,
"prettier.configPath": "",
"prettier.ignorePath": ""
}