Compare commits
4 Commits
eslint-plu
...
use-store-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b5ecc4f7 | ||
|
|
e2dbca194b | ||
|
|
4786a1a43e | ||
|
|
f213a1f6c5 |
@@ -14,6 +14,7 @@ import type {
|
||||
Usable,
|
||||
Thenable,
|
||||
ReactDebugInfo,
|
||||
ReactStore,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {
|
||||
ContextDependency,
|
||||
@@ -481,6 +482,22 @@ 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,
|
||||
@@ -777,6 +794,7 @@ 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,6 +14,7 @@ import type {
|
||||
Thenable,
|
||||
RejectedThenable,
|
||||
Awaited,
|
||||
ReactStore,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {
|
||||
Fiber,
|
||||
@@ -74,6 +75,9 @@ import {
|
||||
isGestureRender,
|
||||
GestureLane,
|
||||
UpdateLanes,
|
||||
includesOnlyTransitions,
|
||||
includesTransitionLane,
|
||||
SomeTransitionLane,
|
||||
} from './ReactFiberLane';
|
||||
import {
|
||||
ContinuousEventPriority,
|
||||
@@ -236,6 +240,13 @@ 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,
|
||||
@@ -1808,6 +1819,141 @@ 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,
|
||||
@@ -1847,7 +1993,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
|
||||
// snapsho and getSnapshot values to bail out. We need to check one more time.
|
||||
// snapshot 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
|
||||
@@ -3832,7 +3978,7 @@ function enqueueRenderPhaseUpdate<S, A>(
|
||||
// TODO: Move to ReactFiberConcurrentUpdates?
|
||||
function entangleTransitionUpdate<S, A>(
|
||||
root: FiberRoot,
|
||||
queue: UpdateQueue<S, A>,
|
||||
queue: UpdateQueue<S, A> | StoreWithSelectorQueue<T>,
|
||||
lane: Lane,
|
||||
): void {
|
||||
if (isTransitionLane(lane)) {
|
||||
@@ -3879,6 +4025,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
useDeferredValue: throwInvalidHookError,
|
||||
useTransition: throwInvalidHookError,
|
||||
useSyncExternalStore: throwInvalidHookError,
|
||||
useStoreWithSelector: throwInvalidHookError,
|
||||
useId: throwInvalidHookError,
|
||||
useHostTransitionStatus: throwInvalidHookError,
|
||||
useFormState: throwInvalidHookError,
|
||||
@@ -3909,6 +4056,7 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
useDeferredValue: mountDeferredValue,
|
||||
useTransition: mountTransition,
|
||||
useSyncExternalStore: mountSyncExternalStore,
|
||||
useStoreWithSelector: mountStoreWithSelector,
|
||||
useId: mountId,
|
||||
useHostTransitionStatus: useHostTransitionStatus,
|
||||
useFormState: mountActionState,
|
||||
@@ -3939,6 +4087,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
useDeferredValue: updateDeferredValue,
|
||||
useTransition: updateTransition,
|
||||
useSyncExternalStore: updateSyncExternalStore,
|
||||
useStoreWithSelector: updateStoreWithSelector,
|
||||
useId: updateId,
|
||||
useHostTransitionStatus: useHostTransitionStatus,
|
||||
useFormState: updateActionState,
|
||||
@@ -3969,6 +4118,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
useDeferredValue: rerenderDeferredValue,
|
||||
useTransition: rerenderTransition,
|
||||
useSyncExternalStore: updateSyncExternalStore,
|
||||
useStoreWithSelector: updateStoreWithSelector,
|
||||
useId: updateId,
|
||||
useHostTransitionStatus: useHostTransitionStatus,
|
||||
useFormState: rerenderActionState,
|
||||
@@ -4130,6 +4280,14 @@ 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();
|
||||
@@ -4297,6 +4455,14 @@ 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();
|
||||
@@ -4464,6 +4630,14 @@ 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();
|
||||
@@ -4631,6 +4805,14 @@ 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();
|
||||
@@ -4816,6 +4998,15 @@ 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();
|
||||
@@ -5008,6 +5199,15 @@ 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();
|
||||
@@ -5200,6 +5400,15 @@ 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,6 +17,7 @@ import type {
|
||||
Awaited,
|
||||
ReactComponentInfo,
|
||||
ReactDebugInfo,
|
||||
ReactStore,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {TransitionTypes} from 'react/src/ReactTransitionType';
|
||||
import type {WorkTag} from './ReactWorkTags';
|
||||
@@ -58,6 +59,7 @@ export type HookType =
|
||||
| 'useDeferredValue'
|
||||
| 'useTransition'
|
||||
| 'useSyncExternalStore'
|
||||
| 'useStoreWithSelector'
|
||||
| 'useId'
|
||||
| 'useCacheRefresh'
|
||||
| 'useOptimistic'
|
||||
@@ -437,6 +439,10 @@ 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>,
|
||||
|
||||
14
packages/react-reconciler/src/__tests__/todo.md
Normal file
14
packages/react-reconciler/src/__tests__/todo.md
Normal file
@@ -0,0 +1,14 @@
|
||||
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.
|
||||
147
packages/react-reconciler/src/__tests__/useStoreWithSelector-test.js
vendored
Normal file
147
packages/react-reconciler/src/__tests__/useStoreWithSelector-test.js
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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,6 +16,7 @@ import type {
|
||||
Usable,
|
||||
ReactCustomFormAction,
|
||||
Awaited,
|
||||
ReactStore,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
import type {ResumableState} from './ReactFizzConfig';
|
||||
@@ -564,6 +565,14 @@ 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;
|
||||
@@ -827,6 +836,7 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
|
||||
useId,
|
||||
// Subscriptions are not setup in a server environment.
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useOptimistic,
|
||||
useActionState,
|
||||
useFormState: useActionState,
|
||||
@@ -851,6 +861,7 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
|
||||
useDeferredValue: clientHookNotSupported,
|
||||
useTransition: clientHookNotSupported,
|
||||
useSyncExternalStore: clientHookNotSupported,
|
||||
useStoreWithSelector: clientHookNotSupported,
|
||||
useId,
|
||||
useHostTransitionStatus,
|
||||
useFormState: useActionState,
|
||||
|
||||
@@ -86,6 +86,7 @@ export const HooksDispatcher: Dispatcher = {
|
||||
useDeferredValue: (unsupportedHook: any),
|
||||
useTransition: (unsupportedHook: any),
|
||||
useSyncExternalStore: (unsupportedHook: any),
|
||||
useStoreWithSelector: (unsupportedHook: any),
|
||||
useId,
|
||||
useHostTransitionStatus: (unsupportedHook: any),
|
||||
useFormState: (unsupportedHook: any),
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createRef,
|
||||
createStore,
|
||||
use,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -52,6 +53,7 @@ export {
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useTransition,
|
||||
useActionState,
|
||||
version,
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createRef,
|
||||
createStore,
|
||||
use,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -53,6 +54,7 @@ export {
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useTransition,
|
||||
useActionState,
|
||||
version,
|
||||
|
||||
@@ -35,6 +35,7 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createRef,
|
||||
createStore,
|
||||
use,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -65,6 +66,7 @@ export {
|
||||
useMemo,
|
||||
useOptimistic,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {lazy} from './ReactLazy';
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {memo} from './ReactMemo';
|
||||
import {cache, cacheSignal} from './ReactCacheClient';
|
||||
import {createStore} from './ReactStore';
|
||||
import {
|
||||
getCacheForType,
|
||||
useCallback,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -83,6 +85,7 @@ export {
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
createStore,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
@@ -95,6 +98,7 @@ export {
|
||||
useOptimistic,
|
||||
useActionState,
|
||||
useSyncExternalStore,
|
||||
useStoreWithSelector,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
StartTransitionOptions,
|
||||
Usable,
|
||||
Awaited,
|
||||
ReactStore,
|
||||
} from 'shared/ReactTypes';
|
||||
import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
@@ -198,6 +199,14 @@ 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
|
||||
|
||||
47
packages/react/src/ReactStore.js
Normal file
47
packages/react/src/ReactStore.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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,3 +381,11 @@ 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,15 +1 @@
|
||||
/**
|
||||
* 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';
|
||||
export default '19.3.0-canary-ea4899e1-20251117';
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"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