Replace react-redux with a preact context binding (#2175)
* Replace react-redux with a preact context binding One of the two packages holding the @preact/compat alias in place, and the contained one: the store is plain redux, and the only react-redux import inside it was a single line re-exporting typed hooks. * Drop the now-unused react-redux types * Subscribe before paint and check once on subscribe Previously, useSelector subscribed to the store inside useEffect, which runs after paint. A dispatch landing between render and that effect was never delivered, since the listener did not exist yet, so the component kept rendering a stale value until some later unrelated dispatch happened to differ from the stale ref. Subscribing in useLayoutEffect narrows the window to before paint, and running the check once immediately on subscribe closes it, which is what react-redux does for the same reason. Adds the first tests for the binding, one of which fails without this change: the store holds 1 while the DOM still shows 0. * Only re-check on subscribe when the state actually moved The subscribe-time check ran unconditionally, so it re-ran the selector at mount. A selector building a fresh object fails Object.is against the value the render already computed, which forced a second render of every connected component: ConnectedRoot and every ConnectedComment, so around 201 extra renders for a 200-comment thread. Reducers return a new root object on every change, so an unchanged state reference means no dispatch was missed and the check has nothing to find. Comparing against the state the render used keeps the property the check exists for while dropping the extra render. The race test still exercises the guarded path, since its dispatch produces a new state object, and a new test pins the mount case: it fails without the guard. Raised by umputun in review.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { mount } from 'enzyme';
|
||||
import createMockStore from 'redux-mock-store';
|
||||
import { Middleware } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider } from 'store/context';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import type { User } from 'common/types';
|
||||
|
||||
@@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||
import { h, Fragment, JSX } from 'preact';
|
||||
import { useState, useRef } from 'preact/hooks';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'store/context';
|
||||
|
||||
import { setUser } from 'store/user/actions';
|
||||
import { Input } from 'components/input';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'preact/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider } from 'store/context';
|
||||
import { Middleware } from 'redux';
|
||||
import createMockStore from 'redux-mock-store';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { h, FunctionComponent, Fragment } from 'preact';
|
||||
import { useState, useCallback, useRef } from 'preact/hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'store/context';
|
||||
import clsx from 'clsx';
|
||||
import { useIntl, defineMessages, IntlShape, FormattedMessage } from 'react-intl';
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { SubscribeByRSS, createSubscribeUrl } from '.';
|
||||
|
||||
import styles from './subscribe-by-rss.module.css';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
jest.mock('store/context', () => ({
|
||||
useSelector: jest.fn((fn) => fn({ theme: 'light' })),
|
||||
}));
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, FunctionComponent, Fragment } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector } from 'store/context';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { User } from 'common/types';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'store/context';
|
||||
import { patchComment } from 'store/comments/actions';
|
||||
import { putCommentVote } from 'common/api';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector } from 'store/context';
|
||||
import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, FunctionComponent, type AriaRole } from 'preact';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import { shallowEqual } from 'store/context';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import clsx from 'clsx';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useMemo } from 'preact/compat';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'store/context';
|
||||
import { BoundActionCreator, BoundActionCreators } from 'utils/actionBinder';
|
||||
|
||||
/** binds actions to dispatch */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector } from 'store/context';
|
||||
|
||||
import { StoreState } from 'store';
|
||||
import { Theme } from 'common/types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { h, render } from 'preact';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider } from 'store/context';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { loadLocale } from 'utils/loadLocale';
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { h } from 'preact';
|
||||
import { render, screen, act } from '@testing-library/preact';
|
||||
import { createStore } from 'redux';
|
||||
|
||||
import { Provider, useSelector } from './context';
|
||||
|
||||
type State = { value: number };
|
||||
|
||||
function reducer(state: State = { value: 0 }, action: { type: string }): State {
|
||||
return action.type === 'bump' ? { value: state.value + 1 } : state;
|
||||
}
|
||||
|
||||
function Display() {
|
||||
const value = useSelector((s: State) => s.value);
|
||||
return <span data-testid="value">{value}</span>;
|
||||
}
|
||||
|
||||
describe('useSelector', () => {
|
||||
it('renders the value at mount', () => {
|
||||
const store = createStore(reducer);
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Display />
|
||||
</Provider>
|
||||
);
|
||||
expect(screen.getByTestId('value').textContent).toBe('0');
|
||||
});
|
||||
|
||||
it('re-renders on a store change', () => {
|
||||
const store = createStore(reducer);
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Display />
|
||||
</Provider>
|
||||
);
|
||||
act(() => {
|
||||
store.dispatch({ type: 'bump' });
|
||||
});
|
||||
expect(screen.getByTestId('value').textContent).toBe('1');
|
||||
});
|
||||
|
||||
// the subscribe-time check must not re-run a selector that builds a fresh object,
|
||||
// or every connected component renders twice at mount
|
||||
it('renders once at mount with an object-building selector', () => {
|
||||
const store = createStore(reducer);
|
||||
let renders = 0;
|
||||
|
||||
function Counting() {
|
||||
const { value } = useSelector((s: State) => ({ value: s.value }));
|
||||
|
||||
renders += 1;
|
||||
|
||||
return <span data-testid="value">{value}</span>;
|
||||
}
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Counting />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(renders).toBe(1);
|
||||
expect(screen.getByTestId('value').textContent).toBe('0');
|
||||
});
|
||||
|
||||
it('does not miss a dispatch that lands between render and subscribe', () => {
|
||||
const store = createStore(reducer);
|
||||
|
||||
// dispatch from inside the component body, which runs during render and so
|
||||
// before the subscribing effect. this is the window a real async resolution
|
||||
// can land in, and a subscription created afterwards never hears about it.
|
||||
let dispatched = false;
|
||||
function Racing() {
|
||||
const value = useSelector((s: State) => s.value);
|
||||
if (!dispatched) {
|
||||
dispatched = true;
|
||||
store.dispatch({ type: 'bump' });
|
||||
}
|
||||
return <span data-testid="value">{value}</span>;
|
||||
}
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Racing />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(store.getState().value).toBe(1);
|
||||
expect(screen.getByTestId('value').textContent).toBe('1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { createContext, h, type ComponentChildren } from 'preact';
|
||||
import { useContext, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||
import type { Store, Dispatch, Action } from 'redux';
|
||||
|
||||
/**
|
||||
* Minimal store binding over preact context, replacing react-redux.
|
||||
*
|
||||
* Only what the widget uses: a provider, dispatch, and a selector with an optional
|
||||
* equality function.
|
||||
*/
|
||||
|
||||
const StoreContext = createContext<Store | null>(null);
|
||||
|
||||
export function Provider<S, A extends Action>({
|
||||
store,
|
||||
children,
|
||||
}: {
|
||||
store: Store<S, A>;
|
||||
children?: ComponentChildren;
|
||||
}) {
|
||||
return <StoreContext.Provider value={store as unknown as Store}>{children}</StoreContext.Provider>;
|
||||
}
|
||||
|
||||
function useStore<S, A extends Action>(): Store<S, A> {
|
||||
const store = useContext(StoreContext);
|
||||
if (!store) {
|
||||
throw new Error('store accessed outside of a Provider');
|
||||
}
|
||||
return store as unknown as Store<S, A>;
|
||||
}
|
||||
|
||||
export function useDispatch<D extends Dispatch = Dispatch>(): D {
|
||||
return useStore().dispatch as D;
|
||||
}
|
||||
|
||||
export function useSelector<S, R>(selector: (state: S) => R, equalityFn?: (a: R, b: R) => boolean): R {
|
||||
const store = useStore<S, Action>();
|
||||
const [, setTick] = useState(0);
|
||||
|
||||
const state = store.getState();
|
||||
const selected = selector(state);
|
||||
|
||||
// refs keep the subscription stable while always comparing against the latest
|
||||
// render's selector and value, so a changed selector cannot resurrect a stale result
|
||||
const selectorRef = useRef(selector);
|
||||
const equalityRef = useRef(equalityFn);
|
||||
const selectedRef = useRef(selected);
|
||||
const stateRef = useRef(state);
|
||||
selectorRef.current = selector;
|
||||
equalityRef.current = equalityFn;
|
||||
selectedRef.current = selected;
|
||||
stateRef.current = state;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const checkForUpdates = () => {
|
||||
const next = selectorRef.current(store.getState());
|
||||
const equal = equalityRef.current
|
||||
? equalityRef.current(selectedRef.current, next)
|
||||
: Object.is(selectedRef.current, next);
|
||||
|
||||
if (!equal) {
|
||||
selectedRef.current = next;
|
||||
setTick((n) => n + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = store.subscribe(checkForUpdates);
|
||||
|
||||
// a dispatch landing between this render and the subscription above is not
|
||||
// delivered, since the listener did not exist yet. check once on subscribe so
|
||||
// that update cannot be lost until some later, unrelated dispatch.
|
||||
//
|
||||
// only when the state actually moved, though: reducers return a new root
|
||||
// object on every change, so an unchanged reference means nothing was missed.
|
||||
// checking unconditionally would re-run the selector, and one building a fresh
|
||||
// object fails Object.is against the render's value and forces a second render
|
||||
// of every connected component at mount
|
||||
if (store.getState() !== stateRef.current) {
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
return unsubscribe;
|
||||
}, [store]);
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function shallowEqual(a: unknown, b: unknown): boolean {
|
||||
if (Object.is(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
|
||||
return (
|
||||
aKeys.length === bKeys.length &&
|
||||
aKeys.every(
|
||||
(key) =>
|
||||
Object.prototype.hasOwnProperty.call(b, key) &&
|
||||
Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export type TypedUseSelectorHook<S> = <R>(selector: (state: S) => R, equalityFn?: (a: R, b: R) => boolean) => R;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore, applyMiddleware, AnyAction, compose, combineReducers } from 'redux';
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import { useDispatch, useSelector, type TypedUseSelectorHook } from './context';
|
||||
import thunk, { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||
import { rootProvider } from './reducers';
|
||||
import { ACTIONS } from './actions';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { h, ComponentChild } from 'preact';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { render as originalRender } from '@testing-library/preact';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider } from 'store/context';
|
||||
|
||||
import en from 'locales/en.json';
|
||||
import { mockStore } from '__stubs__/store';
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
"react": "npm:@preact/compat@^18.3.2",
|
||||
"react-dom": "npm:@preact/compat@^18.3.2",
|
||||
"react-intl": "6.0.5",
|
||||
"react-redux": "^8.0.2",
|
||||
"redux": "^4.2.0",
|
||||
"redux-thunk": "^2.4.1"
|
||||
},
|
||||
@@ -66,7 +65,6 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^18.0.1",
|
||||
"@types/node-emoji": "^1.8.1",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"@types/redux-mock-store": "^1.5.0",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
|
||||
Generated
-77
@@ -122,9 +122,6 @@ importers:
|
||||
react-intl:
|
||||
specifier: 6.0.5
|
||||
version: 6.0.5(@preact/compat@18.3.2(preact@10.29.8))(typescript@5.9.3)
|
||||
react-redux:
|
||||
specifier: ^8.0.2
|
||||
version: 8.1.3(@preact/compat@18.3.2(preact@10.29.8))(@preact/compat@18.3.2(preact@10.29.8))(@types/react@18.3.31)(redux@4.2.1)
|
||||
redux:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.1
|
||||
@@ -198,9 +195,6 @@ importers:
|
||||
'@types/node-emoji':
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.2
|
||||
'@types/react-redux':
|
||||
specifier: ^7.1.34
|
||||
version: 7.1.34
|
||||
'@types/redux-mock-store':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
@@ -1926,18 +1920,12 @@ packages:
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/react-redux@7.1.34':
|
||||
resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==}
|
||||
|
||||
'@types/react@16.14.70':
|
||||
resolution: {integrity: sha512-DM5Q7rSx9G6QYcVvMgxvEurL5P06OxcDNUXrLxlpBzG4ccUewcBCmsztYbxJBobzO8RIwwmjoaD5OsKqdHDuYQ==}
|
||||
|
||||
'@types/react@18.3.31':
|
||||
resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==}
|
||||
|
||||
'@types/react@19.2.17':
|
||||
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
|
||||
|
||||
'@types/redux-mock-store@1.5.0':
|
||||
resolution: {integrity: sha512-jcscBazm6j05Hs6xYCca6psTUBbFT2wqMxT7wZEHAYFxHB/I8jYk7d5msrHUlDiSL02HdTqTmkK2oIV8i3C8DA==}
|
||||
|
||||
@@ -1977,9 +1965,6 @@ packages:
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
'@types/use-sync-external-store@0.0.3':
|
||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||
|
||||
'@types/webpack-env@1.18.8':
|
||||
resolution: {integrity: sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==}
|
||||
|
||||
@@ -5589,27 +5574,6 @@ packages:
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-redux@8.1.3:
|
||||
resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8 || ^17.0 || ^18.0
|
||||
'@types/react-dom': ^16.8 || ^17.0 || ^18.0
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
react-native: '>=0.59'
|
||||
redux: ^4 || ^5.0.0-beta.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
|
||||
read-pkg-up@7.0.1:
|
||||
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6385,11 +6349,6 @@ packages:
|
||||
file-loader:
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -8483,11 +8442,6 @@ snapshots:
|
||||
'@types/react': 18.3.31
|
||||
hoist-non-react-statics: 3.3.2
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7(@types/react@19.2.17)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.17
|
||||
hoist-non-react-statics: 3.3.2
|
||||
|
||||
'@types/html-minifier-terser@6.1.0': {}
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
@@ -8553,13 +8507,6 @@ snapshots:
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/react-redux@7.1.34':
|
||||
dependencies:
|
||||
'@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.17)
|
||||
'@types/react': 19.2.17
|
||||
hoist-non-react-statics: 3.3.2
|
||||
redux: 4.2.1
|
||||
|
||||
'@types/react@16.14.70':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.15
|
||||
@@ -8571,10 +8518,6 @@ snapshots:
|
||||
'@types/prop-types': 15.7.15
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/react@19.2.17':
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/redux-mock-store@1.5.0':
|
||||
dependencies:
|
||||
redux: 4.2.1
|
||||
@@ -8621,8 +8564,6 @@ snapshots:
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.3': {}
|
||||
|
||||
'@types/webpack-env@1.18.8': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
@@ -12830,20 +12771,6 @@ snapshots:
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-redux@8.1.3(@preact/compat@18.3.2(preact@10.29.8))(@preact/compat@18.3.2(preact@10.29.8))(@types/react@18.3.31)(redux@4.2.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
'@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.31)
|
||||
'@types/use-sync-external-store': 0.0.3
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: '@preact/compat@18.3.2(preact@10.29.8)'
|
||||
react-is: 18.3.1
|
||||
use-sync-external-store: 1.6.0(@preact/compat@18.3.2(preact@10.29.8))
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.31
|
||||
react-dom: '@preact/compat@18.3.2(preact@10.29.8)'
|
||||
redux: 4.2.1
|
||||
|
||||
read-pkg-up@7.0.1:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
@@ -13772,10 +13699,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
file-loader: 6.2.0(webpack@5.108.3)
|
||||
|
||||
use-sync-external-store@1.6.0(@preact/compat@18.3.2(preact@10.29.8)):
|
||||
dependencies:
|
||||
react: '@preact/compat@18.3.2(preact@10.29.8)'
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utila@0.4.0: {}
|
||||
|
||||
Reference in New Issue
Block a user