Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/5964-fix-replace-react-redux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Changed

- Removed the `react-redux` dependency, which conflicted with React 19 as a peer dependency, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552), in PR [#5964](https://github.com/microsoft/BotFramework-WebChat/pull/5964), by [@OEvgeny](https://github.com/OEvgeny)
64 changes: 1 addition & 63 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,6 @@
"react-dom": [
"16.8.6",
"using react-dom@16.8.6 to make sure this is the minimum supported version"
],
"react-redux": [
"7",
"react-redux@>7 requires newer version of React"
]
},
"localDependencies": {
Expand Down Expand Up @@ -211,7 +207,6 @@
"math-random": "2.0.1",
"prop-types": "15.8.1",
"react-chain-of-responsibility": "0.4.2",
"react-redux": "7.2.9",
"redux": "5.0.1",
"simple-update-in": "2.2.0",
"use-reduce-memo": "0.1.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/api/src/hooks/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import {
} from 'botframework-webchat-core';
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { Provider } from 'react-redux';
import updateIn from 'simple-update-in';

import { type StrictStyleOptions, type StyleOptions } from '../StyleOptions';
Expand Down Expand Up @@ -85,7 +84,8 @@ import mapMap from '../utils/mapMap';
import normalizeLanguage from '../utils/normalizeLanguage';
import Tracker from './internal/Tracker';
import WebChatAPIContext, { type WebChatAPIContextType } from './internal/WebChatAPIContext';
import WebChatReduxContext, { useDispatch } from './internal/WebChatReduxContext';
import { useDispatch } from './internal/WebChatReduxContext';
import WebChatReduxProvider from './internal/WebChatReduxProvider';
import defaultSelectVoice from './internal/defaultSelectVoice';
import useVoiceHandlers from './internal/useVoiceHandlers';
import activityFallbackPolymiddleware from './middleware/activityFallbackPolymiddleware';
Expand Down Expand Up @@ -811,7 +811,7 @@ const ComposerWithStore = ({ onTelemetry, store, ...props }: ComposerWithStorePr
}, [ponyfill, store]);

return (
<Provider context={WebChatReduxContext} store={memoizedStore}>
<WebChatReduxProvider store={memoizedStore}>
<ReduxStoreComposer store={memoizedStore}>
<GraphProvider store={memoizedStore}>
<ActivityKeyerComposer>
Expand All @@ -821,7 +821,7 @@ const ComposerWithStore = ({ onTelemetry, store, ...props }: ComposerWithStorePr
</ActivityKeyerComposer>
</GraphProvider>
</ReduxStoreComposer>
</Provider>
</WebChatReduxProvider>
);
};

Expand Down
14 changes: 0 additions & 14 deletions packages/api/src/hooks/internal/WebChatReduxContext.js

This file was deleted.

104 changes: 104 additions & 0 deletions packages/api/src/hooks/internal/WebChatReduxContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { createStore } from 'botframework-webchat-core';
import { createContext, useContext, useLayoutEffect, useReducer, useRef } from 'react';

type WebChatReduxStore = ReturnType<typeof createStore>;

type WebChatReduxContextType = {
readonly store: WebChatReduxStore;
readonly subscribe: (listener: () => void) => () => void;
};

const context = createContext<WebChatReduxContextType | undefined>(undefined);

context.displayName = 'WebChatReduxContext';

function increment(value: number): number {
return value + 1;
}

function refEquality<T>(x: T, y: T): boolean {
return x === y;
}

function useWebChatReduxContext(): WebChatReduxContextType {
const value = useContext(context);

if (!value) {
throw new Error('This hook can only be used on a component that is a descendant of <Composer>');
}

return value;
}

function useStore(): WebChatReduxStore {
return useWebChatReduxContext().store;
}

function useDispatch(): WebChatReduxStore['dispatch'] {
return useStore().dispatch;
}

/**
* Selects a value from the Redux store and re-renders when it changes, like `useSelector()` of `react-redux@7`.
*
* Re-renders are scheduled by `useReducer()`, at the same priority as `useState()` of other components that follow the store.
* `useSyncExternalStore()` would re-render with a higher priority on React 18 and later,
* ahead of activities that `<GraphProvider>` propagates through `useState()`.
*/
function useSelector<T>(
selector: (state: ReturnType<WebChatReduxStore['getState']>) => T,
equalityFn: (x: T, y: T) => boolean = refEquality
): T {
const { store, subscribe } = useWebChatReduxContext();
const [, forceRender] = useReducer(increment, 0);
const latestRef = useRef<
| {
readonly equalityFn: (x: T, y: T) => boolean;
readonly selector: (state: ReturnType<WebChatReduxStore['getState']>) => T;
readonly value: T;
}
| undefined
>();

const nextValue = selector(store.getState());

// Keeps the previous value if it is equal, so a custom `equalityFn` returns a stable reference.
const value =
latestRef.current && equalityFn(latestRef.current.value, nextValue) ? latestRef.current.value : nextValue;

useLayoutEffect(() => {
latestRef.current = Object.freeze({ equalityFn, selector, value });
});

useLayoutEffect(() => {
const handleChange = () => {
const { current } = latestRef;

try {
const nextValue = current.selector(store.getState());

if (current.equalityFn(current.value, nextValue)) {
return;
}

// Remembers the new value now, so a dispatch before the re-render (e.g. during render) does not re-render again.
latestRef.current = Object.freeze({ ...current, value: nextValue });
} catch {
// Re-renders to throw the error from the selector.
}

forceRender();
};

// Catches changes between render and subscription.
handleChange();

return subscribe(handleChange);
}, [forceRender, latestRef, store, subscribe]);

return value;
}

export default context;

export { useDispatch, useSelector, useStore, type WebChatReduxContextType, type WebChatReduxStore };
Loading
Loading