diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0db70add..775bf87d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+v1.4.6
+-
+- Устранены проблемы, связанные с работой runSync, все переданные переходы, выполняются
+
v1.4.5
-
- Устранена проблема с синхронизацией состояния роутера при многократном вызове хуков
diff --git a/examples/vk-mini-apps-router-example/src/onboarding/OnboardingThree.tsx b/examples/vk-mini-apps-router-example/src/onboarding/OnboardingThree.tsx
index ae4a04c1..298e52e4 100644
--- a/examples/vk-mini-apps-router-example/src/onboarding/OnboardingThree.tsx
+++ b/examples/vk-mini-apps-router-example/src/onboarding/OnboardingThree.tsx
@@ -8,9 +8,8 @@ export const OnboardingThree = ({ nav }: { nav: string }) => {
const onClick = async () => {
routeNavigator.runSync([
() => routeNavigator.backToFirst(),
- () => routeNavigator.replace('/'),
- () => routeNavigator.push('/'),
- () => routeNavigator.back(),
+ () => routeNavigator.push('/persik'),
+ () => routeNavigator.push('/persik/persik_modal'),
]);
};
return (
@@ -19,7 +18,10 @@ export const OnboardingThree = ({ nav }: { nav: string }) => {
Когда персик огорчен, он выглядит так:
- Закончить
+ 1) Возвращаемся в начало навигации
+ 2) /persik
+ 3) /persik/persik_modal
+ Выполнить runSync
);
diff --git a/package.json b/package.json
index 6b9add74..288660ae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@vkontakte/vk-mini-apps-router",
- "version": "1.4.5",
+ "version": "1.4.6",
"description": "React-роутер для мини-приложений ВКонтакте, построенных на VKUI",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
diff --git a/src/components/RouterProvider.tsx b/src/components/RouterProvider.tsx
index a41bd2ca..e36ffd93 100644
--- a/src/components/RouterProvider.tsx
+++ b/src/components/RouterProvider.tsx
@@ -1,20 +1,19 @@
-import { Action, Router } from '@remix-run/router';
-import { PopoutContext, RouteContext, RouterContext } from '../contexts';
import { ReactElement, ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react';
-import { DefaultRouteNavigator } from '../services/DefaultRouteNavigator';
+import { Action, Router } from '@remix-run/router';
import bridge from '@vkontakte/vk-bridge';
-import { DefaultNotFound } from './DefaultNotFound';
-import { getRouteContext, useForceUpdate } from '../utils/utils';
-import { ViewHistory } from '../services/ViewHistory';
-import { useBlockForwardToModals } from '../hooks/useBlockForwardToModals';
import { SEARCH_PARAM_INFLATE, STATE_KEY_SHOW_POPOUT, UNIVERSAL_URL } from '../const';
-import { RouteNavigator } from '../services/RouteNavigator.type';
-import { TransactionExecutor } from '../services/TransactionExecutor';
-import { fillHistory } from '../utils/fillHistory';
-import { createSearchParams } from '../utils/createSearchParams';
+import { PopoutContext, RouteContext, RouterContext } from '../contexts';
+import { getRouteContext, fillHistory, createSearchParams, getHrefWithoutHash } from '../utils';
+import {
+ DefaultRouteNavigator,
+ ContextThrottleService,
+ TransactionExecutor,
+ RouteNavigator,
+ ViewHistory,
+} from '../services';
+import { useBlockForwardToModals } from '../hooks/useBlockForwardToModals';
+import { DefaultNotFound } from './DefaultNotFound';
import { RouteLeaf } from '../type';
-import { getHrefWithoutHash } from '../utils/getHrefWithoutHash';
-import { ContextThrottleService } from '../services/ContextThrottleService';
export interface RouterProviderProps {
router: Router;
@@ -37,24 +36,19 @@ export function RouterProvider({
useBridge = true,
throttled = true,
}: RouterProviderProps): ReactElement {
- const forceUpdate = useForceUpdate();
const [popout, setPopout] = useState(null);
const [viewHistory] = useState(new ViewHistory());
const [panelsHistory, setPanelsHistory] = useState([]);
- const [transactionExecutor, setTransactionExecutor] = useState(
- new TransactionExecutor(forceUpdate),
- );
const isPopoutShown = router.state.location.state?.[STATE_KEY_SHOW_POPOUT];
const dataRouterContext = useMemo(() => {
const routeNavigator: RouteNavigator = new DefaultRouteNavigator(
router,
viewHistory,
- transactionExecutor,
setPopout,
);
return { router, routeNavigator, viewHistory };
- }, [router, viewHistory, transactionExecutor, setPopout]);
+ }, [router, viewHistory, setPopout]);
const routeContext = useMemo(
() => getRouteContext(router.state, panelsHistory),
@@ -70,6 +64,7 @@ export function RouterProvider({
// Отключаем браузерное восстановление скролла, используем решения от VKUI
history.scrollRestoration = 'manual';
+ TransactionExecutor.resetTransactions();
viewHistory.resetHistory();
viewHistory.updateNavigation({ ...router.state, historyAction: Action.Push });
setPanelsHistory(viewHistory.panelsHistory);
@@ -77,7 +72,7 @@ export function RouterProvider({
router.subscribe((state) => {
viewHistory.updateNavigation(state);
setPanelsHistory(viewHistory.panelsHistory);
- transactionExecutor.doNext();
+ TransactionExecutor.doNext();
});
if (useBridge) {
@@ -86,31 +81,28 @@ export function RouterProvider({
router.navigate(event.detail.data.location, { replace: true });
}
});
+
router.subscribe((state) => {
const href = router.createHref(state.location);
const hrefWithoutHash = getHrefWithoutHash();
const location = href.replace(hrefWithoutHash, '').replace(/^#/, '');
-
bridge.send('VKWebAppSetLocation', { location, replace_state: true });
});
}
- const executor = new TransactionExecutor(forceUpdate);
- setTransactionExecutor(executor);
const searchParams = createSearchParams(router.state.location.search);
const enableFilling = Boolean(searchParams.get(SEARCH_PARAM_INFLATE));
- hierarchy &&
- enableFilling &&
- fillHistory(hierarchy, dataRouterContext.routeNavigator, routeContext, executor);
+ if (hierarchy && enableFilling) {
+ fillHistory(hierarchy, dataRouterContext.routeNavigator, routeContext);
+ }
}, [router]);
useLayoutEffect(() => {
ContextThrottleService.updateThrottledServiceSettings({
interval,
- firstActionDelay: transactionExecutor.initialDelay,
- enable: throttled || Boolean(transactionExecutor.initialDelay),
+ throttled,
});
- }, [transactionExecutor.initialDelay, interval, throttled]);
+ }, [interval, throttled]);
const routeNotFound = Boolean(
!routeContext.match ||
diff --git a/src/services/ContextThrottleService.ts b/src/services/ContextThrottleService.ts
index 77e5d619..146db69c 100644
--- a/src/services/ContextThrottleService.ts
+++ b/src/services/ContextThrottleService.ts
@@ -1,23 +1,22 @@
import { EventBus } from './EventBus';
+import { TransactionExecutor } from './TransactionExecutor';
interface ContextThrottleInfo {
prevValue: unknown;
+ updateTimerId: number;
throttledValue: unknown;
lastUpdateTimestamp: number;
- updateTimerId: number;
}
interface ContextThrottleServiceSettings {
interval: number;
- firstActionDelay: number;
- enable: boolean;
+ throttled: boolean;
}
export class ContextThrottleService {
private static instance?: ContextThrottleService;
- private enable = true;
private interval = 0;
- private firstActionDelay = 0;
+ private throttled = true;
private contextThrottleMap: Record = {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -51,8 +50,7 @@ export class ContextThrottleService {
private getTimeUntilNextUpdate(lastUpdateTimestamp: number) {
const timeSinceLastUpdate = Date.now() - lastUpdateTimestamp;
const delayUntilNextUpdate = this.interval - timeSinceLastUpdate;
- const initialDelay = delayUntilNextUpdate <= 0 ? this.firstActionDelay : 0;
- return Math.max(initialDelay, delayUntilNextUpdate);
+ return delayUntilNextUpdate;
}
private updateContextValue(contextName: string, newValue: T) {
@@ -65,19 +63,25 @@ export class ContextThrottleService {
private throttleUpdateContextValue(contextName: string, newValue: T) {
const contextData = this.getContextThrottleInfoByName(contextName);
+ clearTimeout(contextData.updateTimerId);
+ if (this.isRunSyncActive()) return;
+
const lastUpdateTimestamp = contextData.lastUpdateTimestamp;
const timeUntilNextUpdate = this.getTimeUntilNextUpdate(lastUpdateTimestamp);
if (timeUntilNextUpdate <= 0) {
this.updateContextValue(contextName, newValue);
} else {
- clearTimeout(contextData.updateTimerId);
contextData.updateTimerId = setTimeout(() => {
this.updateContextValue(contextName, newValue);
}, timeUntilNextUpdate);
}
}
+ private isRunSyncActive() {
+ return TransactionExecutor.isRunSyncActive;
+ }
+
public static triggerContextUpdate(contextName: string, newValue: T) {
const throttledService = ContextThrottleService.getInstance();
@@ -85,7 +89,7 @@ export class ContextThrottleService {
return;
}
- if (!throttledService.enable) {
+ if (!throttledService.throttled && !throttledService.isRunSyncActive()) {
throttledService.updateContextValue(contextName, newValue);
} else {
throttledService.throttleUpdateContextValue(contextName, newValue);
@@ -94,8 +98,7 @@ export class ContextThrottleService {
public static updateThrottledServiceSettings(settings: ContextThrottleServiceSettings) {
const throttledService = ContextThrottleService.getInstance();
- throttledService.enable = settings.enable;
throttledService.interval = settings.interval;
- throttledService.firstActionDelay = settings.firstActionDelay;
+ throttledService.throttled = settings.throttled;
}
}
diff --git a/src/services/DefaultRouteNavigator.ts b/src/services/DefaultRouteNavigator.ts
index e91307ae..db739f17 100644
--- a/src/services/DefaultRouteNavigator.ts
+++ b/src/services/DefaultRouteNavigator.ts
@@ -23,7 +23,6 @@ export class DefaultRouteNavigator implements RouteNavigator {
constructor(
router: Router,
private viewHistory: ViewHistory,
- private transactionExecutor: TransactionExecutor,
setPopout: (popout: JSX.Element | null) => void,
) {
this.router = router;
@@ -67,13 +66,13 @@ export class DefaultRouteNavigator implements RouteNavigator {
if (this.viewHistory.position > 0) {
await this.go(-this.viewHistory.position);
} else {
- await this.transactionExecutor.doNext();
+ await TransactionExecutor.doNext();
}
}
public async go(to: number): Promise {
if (to === 0) {
- await this.transactionExecutor.doNext();
+ await TransactionExecutor.doNext();
} else {
await this.router.navigate(to);
}
@@ -81,8 +80,8 @@ export class DefaultRouteNavigator implements RouteNavigator {
public runSync(actions: VoidFunction[]): Promise {
const transaction = new NavigationTransaction(actions);
- this.transactionExecutor.add(transaction);
- this.transactionExecutor.doNext();
+ TransactionExecutor.add(transaction);
+ TransactionExecutor.doNext();
return transaction.donePromise;
}
@@ -108,7 +107,7 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
}
await this.navigate(path, { keepSearchParams: true });
} else {
- await this.transactionExecutor.doNext();
+ await TransactionExecutor.doNext();
}
}
}
@@ -142,7 +141,7 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
await this.router.navigate(-1);
}
} else {
- await this.transactionExecutor.doNext();
+ await TransactionExecutor.doNext();
}
}
diff --git a/src/services/TransactionExecutor.ts b/src/services/TransactionExecutor.ts
index b32c7e74..c334154c 100644
--- a/src/services/TransactionExecutor.ts
+++ b/src/services/TransactionExecutor.ts
@@ -1,29 +1,49 @@
import { NavigationTransaction } from '../entities/NavigationTransaction';
export class TransactionExecutor {
+ private static instance?: TransactionExecutor;
private transactions: NavigationTransaction[] = [];
- constructor(private forceUpdate: () => void) {}
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ private constructor() {}
- get initialDelay(): number {
- return this.transactions.length > 1 ||
- (this.transactions.length > 0 && this.transactions[0].isMultiAction)
- ? 100
- : 0;
+ public static getInstance() {
+ if (!TransactionExecutor.instance) {
+ TransactionExecutor.instance = new TransactionExecutor();
+ }
+
+ return TransactionExecutor.instance;
+ }
+
+ public static get isRunSyncActive() {
+ const transactionExecutor = TransactionExecutor.getInstance();
+ const hasMultipleTransactions = transactionExecutor.transactions.length > 1;
+ const hasSingleMultiActionTransaction =
+ transactionExecutor.transactions.length === 1 &&
+ transactionExecutor.transactions[0].isMultiAction;
+
+ return hasMultipleTransactions || hasSingleMultiActionTransaction;
+ }
+
+ public static add(transaction: NavigationTransaction) {
+ const transactionExecutor = TransactionExecutor.getInstance();
+ transactionExecutor.transactions.push(transaction);
}
- add(transaction: NavigationTransaction): void {
- this.transactions.push(transaction);
- this.forceUpdate();
+ public static resetTransactions() {
+ const transactionExecutor = TransactionExecutor.getInstance();
+ transactionExecutor.transactions = [];
}
- async doNext(): Promise {
+ public static async doNext(): Promise {
+ const transactionExecutor = TransactionExecutor.getInstance();
+ const transactions = transactionExecutor.transactions;
// Нужно делать асинхронно, иначе будет бесконечный цикл навигация-изменение стейта-навигация...
setTimeout(() => {
- if (this.transactions.length) {
- this.transactions[0].doNext();
- if (this.transactions[0].finished) {
- this.transactions.shift();
+ if (transactions.length) {
+ transactions[0].doNext();
+ if (transactions[0].finished) {
+ transactions.shift();
}
}
});
diff --git a/src/services/index.ts b/src/services/index.ts
new file mode 100644
index 00000000..b48a794a
--- /dev/null
+++ b/src/services/index.ts
@@ -0,0 +1,9 @@
+export * from './BridgeService';
+export * from './ContextThrottleService';
+export * from './DefaultRouteNavigator';
+export * from './EventBus';
+export * from './InitialLocation';
+export * from './RouteNavigator.type';
+export * from './TransactionExecutor';
+export * from './ViewHistory';
+export * from './ViewNavigationRecord.type';
diff --git a/src/utils/fillHistory.ts b/src/utils/fillHistory.ts
index 8aac0129..b822f91e 100644
--- a/src/utils/fillHistory.ts
+++ b/src/utils/fillHistory.ts
@@ -25,7 +25,6 @@ export function fillHistory(
config: RouteLeaf[],
routeNavigator: RouteNavigator,
context: RouteContextObject,
- transactionExecutor: TransactionExecutor,
) {
const leafs = flattenBranch(config, []);
const currentLocation = context.state.location;
@@ -49,8 +48,8 @@ export function fillHistory(
() => routeNavigator.push(to),
];
const transaction = new NavigationTransaction(actions);
- transactionExecutor.add(transaction);
- transactionExecutor.doNext();
+ TransactionExecutor.add(transaction);
+ TransactionExecutor.doNext();
}
});
}
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 00000000..5e8805f1
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,9 @@
+export * from './buildPanelPathFromModalMatch';
+export * from './createBrowserRouter';
+export * from './createHashParamRouter';
+export * from './createHashRouter';
+export * from './createSearchParams';
+export * from './fillHistory';
+export * from './getHrefWithoutHash';
+export * from './react-router-override';
+export * from './utils';
diff --git a/src/utils/react-router-override/index.ts b/src/utils/react-router-override/index.ts
new file mode 100644
index 00000000..3efbd081
--- /dev/null
+++ b/src/utils/react-router-override/index.ts
@@ -0,0 +1,5 @@
+export * from './HashParamHistory';
+export * from './UrlHistoryOptions.type';
+export * from './createLocation';
+export * from './getHistoryState';
+export * from './getUrlBasedHistory';
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 5270c29e..757310c1 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -2,7 +2,6 @@ import { AgnosticRouteMatch, Location, Params, RouterState } from '@remix-run/ro
import { RouteContextObject } from '../contexts';
import { PageInternal } from '../type';
import { STATE_KEY_SHOW_MODAL, STATE_KEY_SHOW_POPOUT } from '../const';
-import { useState } from 'react';
export function getParamKeys(path: string | undefined): string[] {
return path?.match(/\/:[^\/]+/g)?.map((param) => param.replace('/', '')) ?? [];
@@ -54,14 +53,6 @@ export function getDisplayName(WrappedComponent: { displayName?: string; name?:
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
-export function useForceUpdate() {
- const [, setState] = useState(0);
-
- return () => {
- setState(Date.now());
- };
-}
-
export function warning(cond: any, message: string) {
if (!cond) {
if (typeof console !== 'undefined') console.warn(message);