Skip to content

Commit edf86d3

Browse files
committed
refactor: refactor RouterHistoryStore to only store the navigation history in its internal state
1 parent 3a5cde0 commit edf86d3

File tree

2 files changed

+81
-133
lines changed

2 files changed

+81
-133
lines changed

packages/router-component-store/src/lib/router-history-store/router-history.store.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ function createTestComponent(name: string, selector: string) {
2121
selector: 'ngw-test-app',
2222
imports: [AsyncPipe, NgIf, RouterLink, RouterOutlet],
2323
template: `
24-
<a
24+
<!-- <a
2525
id="back-link"
2626
*ngIf="routerHistory.previousUrl$ | async as previousUrl"
2727
(click)="onBack()"
2828
>Back</a
29-
>
29+
> -->
30+
<a id="back-link" (click)="onBack()">Back</a>
3031
3132
<a id="home-link" routerLink="/">Home</a>
3233
<a id="about-link" routerLink="about">About</a>
Lines changed: 78 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
11
import { inject, Injectable, Provider } from '@angular/core';
2-
import {
3-
Navigation,
4-
NavigationEnd,
5-
NavigationStart,
6-
Router,
7-
} from '@angular/router';
2+
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
83
import { ComponentStore, provideComponentStore } from '@ngrx/component-store';
9-
import { concatMap, filter, Observable, take } from 'rxjs';
10-
11-
interface RouterHistoryRecord {
12-
readonly id: number;
13-
readonly url: string;
14-
}
4+
import { filter, Observable } from 'rxjs';
155

166
interface RouterHistoryState {
17-
readonly currentIndex: number;
18-
readonly event?: NavigationStart | NavigationEnd;
19-
readonly history: readonly RouterHistoryRecord[];
20-
readonly id: number;
21-
readonly idToRestore?: number;
22-
readonly trigger?: Navigation['trigger'];
7+
readonly history: NavigationHistory;
238
}
249

10+
type CompleteNavigation = readonly [NavigationStart, NavigationEnd];
11+
type NavigationHistory = Record<number, NavigationSequence>;
12+
type NavigationSequence = PendingNavigation | CompleteNavigation;
13+
type PendingNavigation = readonly [NavigationStart];
14+
2515
export function provideRouterHistoryStore(): Provider[] {
2616
return [provideComponentStore(RouterHistoryStore)];
2717
}
@@ -59,11 +49,8 @@ export function provideRouterHistoryStore(): Provider[] {
5949
export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
6050
#router = inject(Router);
6151

62-
#currentIndex$: Observable<number> = this.select(
63-
(state) => state.currentIndex
64-
);
65-
#history$: Observable<readonly RouterHistoryRecord[]> = this.select(
66-
(state) => state.history
52+
#history$ = this.select((state) => state.history).pipe(
53+
filter((history) => Object.keys(history).length > 0)
6754
);
6855
#navigationEnd$: Observable<NavigationEnd> = this.#router.events.pipe(
6956
filter((event): event is NavigationEnd => event instanceof NavigationEnd)
@@ -73,43 +60,49 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
7360
(event): event is NavigationStart => event instanceof NavigationStart
7461
)
7562
);
76-
#imperativeNavigationEnd$: Observable<NavigationEnd> =
77-
this.#navigationStart$.pipe(
78-
filter((event) => event.navigationTrigger === 'imperative'),
79-
concatMap(() => this.#navigationEnd$.pipe(take(1)))
80-
);
81-
#popstateNavigationEnd$: Observable<NavigationEnd> =
82-
this.#navigationStart$.pipe(
83-
filter((event) => event.navigationTrigger === 'popstate'),
84-
concatMap(() => this.#navigationEnd$.pipe(take(1)))
85-
);
8663

87-
currentUrl$: Observable<string> = this.select(
88-
this.#navigationEnd$.pipe(
89-
concatMap(() =>
90-
this.select(
91-
this.#currentIndex$,
92-
this.#history$,
93-
(currentIndex, history) => [currentIndex, history] as const
94-
)
95-
)
96-
),
97-
([currentIndex, history]) => history[currentIndex].url,
64+
#maxCompletedNavigationId$ = this.select(this.#history$, (history) =>
65+
Number(
66+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67+
Object.entries(history)
68+
.reverse()
69+
.find(([, navigation]) => navigation.length === 2)![0]
70+
)
71+
);
72+
#latestCompletedNavigation$ = this.select(
73+
this.#maxCompletedNavigationId$,
74+
this.#history$,
75+
(maxCompletedNavigationId, history) =>
76+
history[maxCompletedNavigationId] as CompleteNavigation,
9877
{
9978
debounce: true,
10079
}
10180
);
102-
previousUrl$: Observable<string | null> = this.select(
103-
this.#navigationEnd$.pipe(
104-
concatMap(() =>
105-
this.select(
106-
this.#currentIndex$,
107-
this.#history$,
108-
(currentIndex, history) => [currentIndex, history] as const
109-
)
110-
)
111-
),
112-
([currentIndex, history]) => history[currentIndex - 1]?.url ?? null,
81+
82+
currentUrl$: Observable<string> = this.select(
83+
this.#latestCompletedNavigation$,
84+
([, end]) => end.urlAfterRedirects
85+
);
86+
previousUrl$: Observable<string | undefined> = this.select(
87+
this.#history$,
88+
this.#maxCompletedNavigationId$,
89+
(history, maxCompletedNavigationId) => {
90+
if (maxCompletedNavigationId === 1) {
91+
return undefined;
92+
}
93+
94+
const [completedNavigationSourceStart] = this.#getNavigationSource(
95+
maxCompletedNavigationId,
96+
history
97+
);
98+
const previousNavigationId = completedNavigationSourceStart.id - 1;
99+
const [, previousNavigationSourceEnd] = this.#getNavigationSource(
100+
previousNavigationId,
101+
history
102+
);
103+
104+
return previousNavigationSourceEnd.urlAfterRedirects;
105+
},
113106
{
114107
debounce: true,
115108
}
@@ -118,95 +111,49 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
118111
constructor() {
119112
super(initialState);
120113

121-
this.#updateRouterHistoryOnNavigationStart(this.#navigationStart$);
122-
this.#updateRouterHistoryOnImperativeNavigationEnd(
123-
this.#imperativeNavigationEnd$
124-
);
125-
this.#updateRouterHistoryOnPopstateNavigationEnd(
126-
this.#popstateNavigationEnd$
127-
);
114+
this.#addNavigationStart(this.#navigationStart$);
115+
this.#addNavigationEnd(this.#navigationEnd$);
128116
}
129117

130-
/**
131-
* Update router history on imperative navigation end (`Router#navigate`,
132-
* `Router#navigateByUrl`, or `RouterLink` click).
133-
*/
134-
#updateRouterHistoryOnImperativeNavigationEnd = this.updater<NavigationEnd>(
135-
(state, event): RouterHistoryState => {
136-
let currentIndex = state.currentIndex;
137-
let history = state.history;
138-
// remove all events in history that come after the current index
139-
history = [
140-
...history.slice(0, currentIndex + 1),
141-
// add the new event to the end of the history
142-
{
143-
id: state.id,
144-
url: event.urlAfterRedirects,
145-
},
146-
];
147-
// set the new event as our current history index
148-
currentIndex = history.length - 1;
149-
150-
return {
151-
...state,
152-
currentIndex,
153-
event,
154-
history,
155-
};
156-
}
118+
#addNavigationEnd = this.updater<NavigationEnd>(
119+
(state, event): RouterHistoryState => ({
120+
...state,
121+
history: {
122+
...state.history,
123+
[event.id]: [state.history[event.id][0], event],
124+
},
125+
})
157126
);
158127

159-
#updateRouterHistoryOnNavigationStart = this.updater<NavigationStart>(
128+
#addNavigationStart = this.updater<NavigationStart>(
160129
(state, event): RouterHistoryState => ({
161130
...state,
162-
id: event.id,
163-
idToRestore: event.restoredState?.navigationId ?? undefined,
164-
event,
165-
trigger: event.navigationTrigger,
131+
history: {
132+
...state.history,
133+
[event.id]: [event],
134+
},
166135
})
167136
);
168137

169-
/**
170-
* Update router history on browser navigation end (back, forward, and other
171-
* `popstate` or `pushstate` events).
172-
*/
173-
#updateRouterHistoryOnPopstateNavigationEnd = this.updater<NavigationEnd>(
174-
(state, event): RouterHistoryState => {
175-
let currentIndex = 0;
176-
let { history } = state;
177-
// get the history item that references the idToRestore
178-
const historyIndexToRestore = history.findIndex(
179-
(historyRecord) => historyRecord.id === state.idToRestore
180-
);
181-
182-
// if found, set the current index to that history item and update the id
183-
if (historyIndexToRestore > -1) {
184-
currentIndex = historyIndexToRestore;
185-
history = [
186-
...history.slice(0, historyIndexToRestore),
187-
{
188-
...history[historyIndexToRestore],
189-
id: state.id,
190-
},
191-
...history.slice(historyIndexToRestore + 1),
138+
#getNavigationSource(
139+
navigationId: number,
140+
history: NavigationHistory
141+
): CompleteNavigation {
142+
let navigation = history[navigationId];
143+
144+
while (navigation[0].navigationTrigger === 'popstate') {
145+
navigation =
146+
history[
147+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148+
navigation[0].restoredState!.navigationId
192149
];
193-
}
194-
195-
return {
196-
...state,
197-
currentIndex,
198-
event,
199-
history,
200-
};
150+
navigationId = navigation[0].id;
201151
}
202-
);
152+
153+
return navigation as CompleteNavigation;
154+
}
203155
}
204156

205157
export const initialState: RouterHistoryState = {
206-
currentIndex: 0,
207-
event: undefined,
208158
history: [],
209-
id: 0,
210-
idToRestore: 0,
211-
trigger: undefined,
212159
};

0 commit comments

Comments
 (0)