Compare commits
1 Commits
use-store-
...
pr35147
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efd4d049b1 |
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
213
packages/react-reconciler/src/ReactFiberHooks.js
vendored
213
packages/react-reconciler/src/ReactFiberHooks.js
vendored
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
11
packages/react-server/src/ReactFizzHooks.js
vendored
11
packages/react-server/src/ReactFizzHooks.js
vendored
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -21,7 +21,6 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createRef,
|
||||
createStore,
|
||||
use,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -53,7 +52,6 @@ export {
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useTransition,
|
||||
useActionState,
|
||||
version,
|
||||
|
||||
@@ -21,7 +21,6 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createRef,
|
||||
createStore,
|
||||
use,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -54,7 +53,6 @@ export {
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useTransition,
|
||||
useActionState,
|
||||
version,
|
||||
|
||||
@@ -35,7 +35,6 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createRef,
|
||||
createStore,
|
||||
use,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -66,7 +65,6 @@ export {
|
||||
useMemo,
|
||||
useOptimistic,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user