From a91e322d5ca7663c270ed5c623acef776f999cc0 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Fri, 21 Aug 2026 06:47:08 +0100 Subject: [PATCH] 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. --- .../components/auth-panel/auth-panel.test.tsx | 2 +- .../remark42/app/components/auth/auth.tsx | 2 +- .../comment-form__subscribe-by-email.test.tsx | 2 +- .../comment-form__subscribe-by-email.tsx | 2 +- .../comment-form__subscribe-by-rss.test.tsx | 2 +- .../comment-form__subscribe-by-telegram.tsx | 2 +- .../app/components/comment/comment-votes.tsx | 2 +- .../remark42/app/components/root/root.tsx | 2 +- .../remark42/app/components/thread/thread.tsx | 2 +- frontend/apps/remark42/app/hooks/useAction.ts | 2 +- frontend/apps/remark42/app/hooks/useTheme.ts | 2 +- frontend/apps/remark42/app/remark.tsx | 2 +- .../apps/remark42/app/store/context.test.tsx | 91 +++++++++++++++ frontend/apps/remark42/app/store/context.tsx | 109 ++++++++++++++++++ frontend/apps/remark42/app/store/index.ts | 2 +- frontend/apps/remark42/app/tests/utils.tsx | 2 +- frontend/apps/remark42/package.json | 2 - frontend/pnpm-lock.yaml | 77 ------------- 18 files changed, 214 insertions(+), 93 deletions(-) create mode 100644 frontend/apps/remark42/app/store/context.test.tsx create mode 100644 frontend/apps/remark42/app/store/context.tsx diff --git a/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx b/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx index 6902508b..fc3aad89 100644 --- a/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/auth/auth.tsx b/frontend/apps/remark42/app/components/auth/auth.tsx index 12f9b31f..bd98e6ac 100644 --- a/frontend/apps/remark42/app/components/auth/auth.tsx +++ b/frontend/apps/remark42/app/components/auth/auth.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx index 9459d0f6..54124001 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx index 37d7ebd3..63e51e82 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.test.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.test.tsx index 5e650742..8b0eb867 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.test.tsx +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.test.tsx @@ -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' })), })); diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx index c504508d..410f84af 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/comment/comment-votes.tsx b/frontend/apps/remark42/app/components/comment/comment-votes.tsx index 76da83e0..168fee82 100644 --- a/frontend/apps/remark42/app/components/comment/comment-votes.tsx +++ b/frontend/apps/remark42/app/components/comment/comment-votes.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/root/root.tsx b/frontend/apps/remark42/app/components/root/root.tsx index 5bb065d0..ab29254b 100644 --- a/frontend/apps/remark42/app/components/root/root.tsx +++ b/frontend/apps/remark42/app/components/root/root.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/components/thread/thread.tsx b/frontend/apps/remark42/app/components/thread/thread.tsx index 2478d02f..89c856f3 100644 --- a/frontend/apps/remark42/app/components/thread/thread.tsx +++ b/frontend/apps/remark42/app/components/thread/thread.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/hooks/useAction.ts b/frontend/apps/remark42/app/hooks/useAction.ts index 3f051588..64165aba 100644 --- a/frontend/apps/remark42/app/hooks/useAction.ts +++ b/frontend/apps/remark42/app/hooks/useAction.ts @@ -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 */ diff --git a/frontend/apps/remark42/app/hooks/useTheme.ts b/frontend/apps/remark42/app/hooks/useTheme.ts index 38eec0f4..ae8d13de 100644 --- a/frontend/apps/remark42/app/hooks/useTheme.ts +++ b/frontend/apps/remark42/app/hooks/useTheme.ts @@ -1,4 +1,4 @@ -import { useSelector } from 'react-redux'; +import { useSelector } from 'store/context'; import { StoreState } from 'store'; import { Theme } from 'common/types'; diff --git a/frontend/apps/remark42/app/remark.tsx b/frontend/apps/remark42/app/remark.tsx index c129071a..5df11131 100644 --- a/frontend/apps/remark42/app/remark.tsx +++ b/frontend/apps/remark42/app/remark.tsx @@ -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'; diff --git a/frontend/apps/remark42/app/store/context.test.tsx b/frontend/apps/remark42/app/store/context.test.tsx new file mode 100644 index 00000000..179ed3fd --- /dev/null +++ b/frontend/apps/remark42/app/store/context.test.tsx @@ -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 {value}; +} + +describe('useSelector', () => { + it('renders the value at mount', () => { + const store = createStore(reducer); + render( + + + + ); + expect(screen.getByTestId('value').textContent).toBe('0'); + }); + + it('re-renders on a store change', () => { + const store = createStore(reducer); + render( + + + + ); + 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 {value}; + } + + render( + + + + ); + + 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 {value}; + } + + render( + + + + ); + + expect(store.getState().value).toBe(1); + expect(screen.getByTestId('value').textContent).toBe('1'); + }); +}); diff --git a/frontend/apps/remark42/app/store/context.tsx b/frontend/apps/remark42/app/store/context.tsx new file mode 100644 index 00000000..815f7669 --- /dev/null +++ b/frontend/apps/remark42/app/store/context.tsx @@ -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(null); + +export function Provider({ + store, + children, +}: { + store: Store; + children?: ComponentChildren; +}) { + return {children}; +} + +function useStore(): Store { + const store = useContext(StoreContext); + if (!store) { + throw new Error('store accessed outside of a Provider'); + } + return store as unknown as Store; +} + +export function useDispatch(): D { + return useStore().dispatch as D; +} + +export function useSelector(selector: (state: S) => R, equalityFn?: (a: R, b: R) => boolean): R { + const store = useStore(); + 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)[key], (b as Record)[key]) + ) + ); +} + +export type TypedUseSelectorHook = (selector: (state: S) => R, equalityFn?: (a: R, b: R) => boolean) => R; diff --git a/frontend/apps/remark42/app/store/index.ts b/frontend/apps/remark42/app/store/index.ts index 8f2ba6ce..761c4cb9 100644 --- a/frontend/apps/remark42/app/store/index.ts +++ b/frontend/apps/remark42/app/store/index.ts @@ -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'; diff --git a/frontend/apps/remark42/app/tests/utils.tsx b/frontend/apps/remark42/app/tests/utils.tsx index 415aacac..fba4ea2e 100644 --- a/frontend/apps/remark42/app/tests/utils.tsx +++ b/frontend/apps/remark42/app/tests/utils.tsx @@ -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'; diff --git a/frontend/apps/remark42/package.json b/frontend/apps/remark42/package.json index 647b7c49..fa2cd597 100644 --- a/frontend/apps/remark42/package.json +++ b/frontend/apps/remark42/package.json @@ -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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 31d8b310..5b825974 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -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: {}