navigation-stack 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +144 -282
  3. package/karma.conf.cjs +1 -1
  4. package/lib/cjs/NavigationStack.js +138 -49
  5. package/lib/cjs/data-storage/DataStorage.js +7 -6
  6. package/lib/cjs/environment/InMemoryEnvironment.js +6 -0
  7. package/lib/cjs/{session/ServerSideRenderSession.js → environment/ServerSideRenderEnvironment.js} +5 -6
  8. package/lib/cjs/environment/WebBrowserEnvironment.js +6 -0
  9. package/lib/cjs/environment/log/InMemoryLog.js +23 -0
  10. package/lib/cjs/environment/log/WebBrowserLog.js +22 -0
  11. package/lib/cjs/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  12. package/lib/cjs/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  13. package/lib/cjs/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  14. package/lib/cjs/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +2 -2
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  16. package/lib/cjs/getLocationBaseFromLocation.js +14 -0
  17. package/lib/cjs/getLocationUrl.js +3 -5
  18. package/lib/cjs/index.js +10 -16
  19. package/lib/cjs/navigationBlockers.js +34 -32
  20. package/lib/cjs/navigationBlockersEvaluation.js +150 -0
  21. package/lib/cjs/parseInputLocation.js +10 -3
  22. package/lib/cjs/parseQueryFromSearch.js +3 -6
  23. package/lib/cjs/parseQueryString.js +77 -0
  24. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +7 -6
  25. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +31 -27
  26. package/lib/cjs/scroll-position/ScrollPositionSaver.js +6 -4
  27. package/lib/cjs/session/Session.js +61 -26
  28. package/lib/cjs/session/subscription/Subscription.js +36 -18
  29. package/lib/cjs/stringifyQuery.js +66 -0
  30. package/lib/cjs/stringifyQueryAsSearch.js +14 -0
  31. package/lib/esm/NavigationStack.js +138 -49
  32. package/lib/esm/data-storage/DataStorage.js +7 -6
  33. package/lib/esm/environment/InMemoryEnvironment.js +6 -0
  34. package/lib/esm/environment/ServerSideRenderEnvironment.js +10 -0
  35. package/lib/esm/environment/WebBrowserEnvironment.js +6 -0
  36. package/lib/esm/environment/log/InMemoryLog.js +17 -0
  37. package/lib/esm/environment/log/WebBrowserLog.js +16 -0
  38. package/lib/esm/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  39. package/lib/esm/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  40. package/lib/esm/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  41. package/lib/esm/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  42. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  43. package/lib/esm/getLocationBaseFromLocation.js +9 -0
  44. package/lib/esm/getLocationUrl.js +2 -5
  45. package/lib/esm/index.js +5 -8
  46. package/lib/esm/navigationBlockers.js +34 -32
  47. package/lib/esm/navigationBlockersEvaluation.js +145 -0
  48. package/lib/esm/parseInputLocation.js +9 -3
  49. package/lib/esm/parseQueryFromSearch.js +2 -6
  50. package/lib/esm/parseQueryString.js +72 -0
  51. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +7 -6
  52. package/lib/esm/scroll-position/ScrollPositionRestoration.js +31 -27
  53. package/lib/esm/scroll-position/ScrollPositionSaver.js +6 -4
  54. package/lib/esm/session/Session.js +61 -26
  55. package/lib/esm/session/subscription/Subscription.js +36 -18
  56. package/lib/esm/stringifyQuery.js +61 -0
  57. package/lib/esm/stringifyQueryAsSearch.js +8 -0
  58. package/lib/index.d.ts +180 -34
  59. package/package.json +4 -7
  60. package/src/NavigationStack.js +166 -56
  61. package/src/data-storage/DataStorage.js +9 -6
  62. package/src/environment/InMemoryEnvironment.js +6 -0
  63. package/src/environment/ServerSideRenderEnvironment.js +10 -0
  64. package/src/environment/WebBrowserEnvironment.js +6 -0
  65. package/src/environment/log/InMemoryLog.js +20 -0
  66. package/src/environment/log/WebBrowserLog.js +18 -0
  67. package/src/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  68. package/src/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  69. package/src/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  70. package/src/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  71. package/src/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  72. package/src/getLocationBaseFromLocation.js +7 -0
  73. package/src/getLocationUrl.js +2 -5
  74. package/src/index.js +10 -13
  75. package/src/navigationBlockers.js +55 -34
  76. package/src/navigationBlockersEvaluation.js +161 -0
  77. package/src/parseInputLocation.js +10 -3
  78. package/src/parseQueryFromSearch.js +2 -6
  79. package/src/parseQueryString.js +81 -0
  80. package/src/scroll-position/ScrollPositionAutoSaver.js +10 -6
  81. package/src/scroll-position/ScrollPositionRestoration.js +36 -30
  82. package/src/scroll-position/ScrollPositionSaver.js +6 -4
  83. package/src/scroll-position/index.js +1 -1
  84. package/src/session/Session.js +68 -24
  85. package/src/session/subscription/Subscription.js +36 -11
  86. package/src/stringifyQuery.js +71 -0
  87. package/src/stringifyQueryAsSearch.js +9 -0
  88. package/test/NavigationStack.addBasePath.test.js +50 -0
  89. package/test/{redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockNonProgrammaticNavigationIfRequired.test.js} +51 -63
  90. package/test/{redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockProgrammaticNavigationIfRequired.test.js} +98 -78
  91. package/test/NavigationStack.general.test.js +68 -0
  92. package/test/NavigationStack.parseInputLocation.test.js +52 -0
  93. package/test/NavigationStack.removeBasePath.test.js +69 -0
  94. package/test/NavigationStack.test.js +97 -29
  95. package/test/data-storage/LocationDataStorage.test.js +3 -2
  96. package/test/index.js +7 -31
  97. package/test/index.test.js +4 -5
  98. package/test/parseQueryFromSearch.test.js +19 -0
  99. package/test/parseQueryString.test.js +18 -0
  100. package/test/scroll-position/ScrollPositionRestoration.test.js +34 -13
  101. package/test/scroll-position/createApp.js +8 -8
  102. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +4 -4
  103. package/test/session/{InMemorySession.test.js → Session.InMemoryEnvironment.test.js} +10 -9
  104. package/test/session/{ServerSession.test.js → Session.ServerSideRenderEnvironment.test.js} +5 -4
  105. package/test/session/{WebBrowserSession.test.js → Session.WebBrowserEnvironment.test.js} +63 -13
  106. package/test/shouldWarn.js +44 -0
  107. package/test/stringifyQuery.test.js +65 -0
  108. package/types/index.d.ts +180 -34
  109. package/types/tsconfig.json +0 -1
  110. package/data-storage/package.json +0 -7
  111. package/lib/cjs/createSearchFromQuery.js +0 -13
  112. package/lib/cjs/debug.js +0 -12
  113. package/lib/cjs/redux/ActionTypes.js +0 -14
  114. package/lib/cjs/redux/ActionTypesInternal.js +0 -8
  115. package/lib/cjs/redux/Actions.js +0 -28
  116. package/lib/cjs/redux/createMiddlewares.js +0 -60
  117. package/lib/cjs/redux/index.js +0 -13
  118. package/lib/cjs/redux/internalLocationReducer.js +0 -14
  119. package/lib/cjs/redux/locationReducer.js +0 -13
  120. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -32
  121. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -113
  122. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  123. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -30
  124. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -73
  125. package/lib/cjs/redux/middleware/navigationOperationMiddleware.js +0 -40
  126. package/lib/cjs/redux/middleware/parseInputLocationMiddleware.js +0 -29
  127. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +0 -34
  128. package/lib/cjs/session/InMemorySession.js +0 -22
  129. package/lib/cjs/session/WebBrowserSession.js +0 -20
  130. package/lib/data-storage/index.d.ts +0 -35
  131. package/lib/esm/createSearchFromQuery.js +0 -8
  132. package/lib/esm/debug.js +0 -7
  133. package/lib/esm/redux/ActionTypes.js +0 -9
  134. package/lib/esm/redux/ActionTypesInternal.js +0 -3
  135. package/lib/esm/redux/Actions.js +0 -22
  136. package/lib/esm/redux/createMiddlewares.js +0 -54
  137. package/lib/esm/redux/index.js +0 -4
  138. package/lib/esm/redux/internalLocationReducer.js +0 -8
  139. package/lib/esm/redux/locationReducer.js +0 -7
  140. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  141. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -108
  142. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -88
  143. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -25
  144. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -68
  145. package/lib/esm/redux/middleware/navigationOperationMiddleware.js +0 -35
  146. package/lib/esm/redux/middleware/parseInputLocationMiddleware.js +0 -24
  147. package/lib/esm/redux/middleware/updateLocationMiddleware.js +0 -28
  148. package/lib/esm/session/InMemorySession.js +0 -15
  149. package/lib/esm/session/ServerSideRenderSession.js +0 -11
  150. package/lib/esm/session/WebBrowserSession.js +0 -13
  151. package/lib/redux/index.d.ts +0 -90
  152. package/lib/scroll-position/index.d.ts +0 -107
  153. package/redux/package.json +0 -7
  154. package/scroll-position/package.json +0 -7
  155. package/src/createSearchFromQuery.js +0 -9
  156. package/src/debug.js +0 -8
  157. package/src/redux/ActionTypes.js +0 -9
  158. package/src/redux/ActionTypesInternal.js +0 -3
  159. package/src/redux/Actions.js +0 -27
  160. package/src/redux/createMiddlewares.js +0 -65
  161. package/src/redux/index.js +0 -4
  162. package/src/redux/internalLocationReducer.js +0 -9
  163. package/src/redux/locationReducer.js +0 -8
  164. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  165. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -119
  166. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  167. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -26
  168. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -72
  169. package/src/redux/middleware/navigationOperationMiddleware.js +0 -34
  170. package/src/redux/middleware/parseInputLocationMiddleware.js +0 -23
  171. package/src/redux/middleware/updateLocationMiddleware.js +0 -28
  172. package/src/session/InMemorySession.js +0 -13
  173. package/src/session/ServerSideRenderSession.js +0 -9
  174. package/src/session/WebBrowserSession.js +0 -13
  175. package/test/middlewareTestUtil.js +0 -31
  176. package/test/redux/Action.test.js +0 -73
  177. package/test/redux/ActionTypes.test.js +0 -13
  178. package/test/redux/createMiddlewares.test.js +0 -96
  179. package/test/redux/index.test.js +0 -10
  180. package/test/redux/locationReducer.test.js +0 -39
  181. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +0 -40
  182. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +0 -51
  183. package/test/redux/middleware/navigationOperationMiddleware.test.js +0 -78
  184. package/test/redux/middleware/parseInputLocationMiddleware.test.js +0 -62
  185. package/test/testUtil.js +0 -3
  186. package/types/data-storage/index.d.ts +0 -35
  187. package/types/redux/index.d.ts +0 -90
  188. package/types/scroll-position/index.d.ts +0 -107
  189. /package/lib/cjs/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  190. /package/lib/cjs/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  191. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  192. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  193. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  194. /package/lib/cjs/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  195. /package/lib/cjs/{session → environment}/navigation/operation/operations.js +0 -0
  196. /package/lib/esm/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  197. /package/lib/esm/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  198. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  199. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  200. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  201. /package/lib/esm/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  202. /package/lib/esm/{session → environment}/navigation/operation/operations.js +0 -0
  203. /package/src/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  204. /package/src/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  205. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  206. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  207. /package/src/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  208. /package/src/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  209. /package/src/{session → environment}/navigation/operation/operations.js +0 -0
  210. /package/test/{parseInputLocationMiddleware.test.js → parseInputLocation.test.js} +0 -0
package/README.md CHANGED
@@ -3,16 +3,18 @@
3
3
  [![npm version](https://img.shields.io/npm/v/navigation-stack.svg?style=flat-square)](https://www.npmjs.com/package/navigation-stack)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/navigation-stack.svg?style=flat-square)](https://www.npmjs.com/package/navigation-stack)
5
5
 
6
- Handles navigation in a web browser. Represents web browser navigation history as a "stack" data structure. Provides operations to perform programmatic navigation such as "push" (go to new URL), "replace" (redirect to new URL), "shift" (rewind to a previously visited URL). Provides a subscription mechanism to get notified on location changes.
6
+ Navigation in a Single-Page Application.
7
7
 
8
- Also supports automatic [scroll position restoration](#scroll-position-restoration) on "Back"/"Forward" navigation.
9
-
10
- Originally forked from [`farce`](http://npmjs.com/package/farce) package to fix a couple of small bugs ([1](https://github.com/4Catalyzer/farce/issues/483), [2](https://github.com/4Catalyzer/farce/issues/491)). Then merged it with [`scroll-behavior`](http://npmjs.com/package/scroll-behavior) package to fix a couple of small bugs ([1](https://github.com/taion/scroll-behavior/issues/215), [2](https://github.com/taion/scroll-behavior/pull/472)). Then decided to completely rewrite the entire code and changed the API to my liking.
8
+ * Represents web browser navigation history as a "stack" data structure.
9
+ * Provides operations to perform programmatic navigation such as "push" (go to new URL), "replace" (redirect to new URL), "shift" (rewind to a previously visited URL).
10
+ * Provides a subscription mechanism to get notified on location changes.
11
+ * Supports automatic [scroll position restoration](#scroll-position-restoration) on "Back"/"Forward" navigation.
12
+ * If you're using React, see [`navigation-stack-react`](http://npmjs.com/package/navigation-stack-react) package.
11
13
 
12
14
  ## Install
13
15
 
14
16
  ```
15
- npm install navigation-stack
17
+ npm install navigation-stack --save
16
18
  ```
17
19
 
18
20
  ## Use
@@ -22,17 +24,17 @@ Any changes to a `NavigationStack` instance are "magically" reflected in the web
22
24
  Start by creating a `NavigationStack` instance.
23
25
 
24
26
  ```js
25
- import { NavigationStack, WebBrowserSession } from 'navigation-stack'
27
+ import { NavigationStack, WebBrowserEnvironment } from 'navigation-stack'
26
28
 
27
29
  // Create a `NavigationStack` instance.
28
- // It should be tied to a navigation "session".
29
- const navigationStack = new NavigationStack(new WebBrowserSession())
30
+ const navigationStack = new NavigationStack(WebBrowserEnvironment)
30
31
  ```
31
32
 
32
33
  Then subscribe to changes:
33
34
 
34
35
  ```js
35
36
  // Subscribe to location changes.
37
+ // The listener function will be called immediately after the current location has changed.
36
38
  // The first call happens for the initial location.
37
39
  // Next calls will happen in case of navigation.
38
40
  const unsubscribe = navigationStack.subscribe((location) => {
@@ -45,6 +47,7 @@ Now ready to perform navigation actions.
45
47
 
46
48
  ```js
47
49
  // Sets the initial location.
50
+ // No argument when using `WebBrowserEnvironment`.
48
51
  navigationStack.init()
49
52
 
50
53
  // Sets the `location` to be a new location.
@@ -139,135 +142,63 @@ Current `location` object has all the properties of a [standard web browser loca
139
142
  * `key: string` — A string ID of the location that is guaranteed to be unique within the session's limits and could be used as a "key" to store any supplementary data associated to this location.
140
143
  * `index: number` — The index of the location in the navigation stack, starting with `0` for the initial location.
141
144
 
142
- <!-- ## Subscribe to Location Changes -->
143
-
144
- <!--
145
- One could use Redux'es standard [subscription mechanisms](https://redux.js.org/api/store#subscribelistener) to immediately get notified of current location changes.
146
-
147
- ```js
148
- let currentLocation
149
-
150
- // Create a Redux store.
151
- const store = createStore(
152
- locationReducer, // Reducer function. For example, `locationReducer()`.
153
- applyMiddleware(...createMiddlewares(new WebBrowserSession()))
154
- )
155
-
156
- // Subscribe to any potential Redux state changes.
157
- const unsubscribe = store.subscribe(() => {
158
- const previousLocation = currentLocation
159
- currentLocation = store.getState() // In case of using `locationReducer()`.
160
- if (currentLocation !== previousLocation) {
161
- // The first time is for the initial location.
162
- // Next times will happen in case of navigation.
163
- console.log('Location has changed')
164
- }
165
- })
166
-
167
- // Initialize navigation with an initial location.
168
- //
169
- // It will trigger the listener.
170
- //
171
- store.dispatch(Actions.init(window.location))
145
+ ## Scroll Position Restoration
172
146
 
173
- // Stop listening to current location changes.
174
- unsubscribe()
175
- ```
176
- -->
147
+ By default, `NavigationStack` doesn't do anything with the scroll position when performing navigation. This means that it neither scrolls to the top of the page when calling `.push()` or `.replace()`, nor restores the previous scroll position on "Back" or "Forward" navigation, including `.shift()` navigation.
177
148
 
178
- <!--
179
- One could subscribe to location changes by calling `navigationStack.subscribe()`.
149
+ To fix that, enable automatic scroll position management feature by passing `manageScrollPosition: true` parameter when creating a `NavigationStack` instance, and then call `.locationRendered(location)` every time a different location has been rendered (including the initial location) immediately after it has been rendered.
180
150
 
181
151
  ```js
182
- // Create a `NavigationStack` instance.
183
- // It should be tied to a navigation "session".
184
- const navigationStack = new NavigationStack(new WebBrowserSession())
152
+ import { NavigationStack, WebBrowserEnvironment } from 'navigation-stack'
185
153
 
186
- // Subscribe to location changes.
187
- // The first call happens for the initial location.
188
- // Next calls will happen in case of navigation.
189
- const unsubscribe = navigationStack.subscribe((location) => {
190
- console.log('Current location', location)
154
+ // Create a `NavigationStack` instance with a `manageScrollPosition: true` option.
155
+ const navigationStack = new NavigationStack(WebBrowserEnvironment, {
156
+ manageScrollPosition: true
191
157
  })
192
158
 
193
- // Navigate to a new location.
194
- // It will trigger the listener.
195
- navigationStack.push('/new-location')
196
-
197
- // Stop listening to location changes.
198
- unsubscribe()
199
- ```
200
- -->
201
-
202
- <!--
203
- ## Why Redux?
204
-
205
- Why complicate things by providing "middlewares", "actions" and a "reducer" when it could be just a conventional API? That's because always knowing the "current location" means having to deal with "state management" in one way or another, and the simplest and most popular "state management" toolkit to date seems to be Redux.
206
-
207
- If it was just about dispatching the `Actions` then of course it wouldn't require any "state management". But it's the "get current location" piece that changes the whole picture. One could say that using Redux for such a simple task is an overkill but actually reinventing a wheel is what I would consider "overkill". It's like crafting your own screwdriver just because the one from Walmart feels too bulky.
208
- -->
209
-
210
- ## Scroll Position Restoration
159
+ //----------------------------------------------------------------------------------------
211
160
 
212
- Pass `maintainScrollPosition: true` option to keep track of scroll position on every page and then automatically restore it on "Back" or "Forward" navigation.
161
+ function onLocationChange(location) {
162
+ // Render the page.
163
+ if (location.pathname === '/initial') {
164
+ document.body.innerHTML = '<div> Initial Location </div>'
165
+ } else if (location.pathame === '/new') {
166
+ document.body.innerHTML = '<div> New Location </div>'
167
+ } else {
168
+ throw new Error(`Unknown location: ${location.pathname}`)
169
+ }
213
170
 
214
- ```js
215
- import { NavigationStack, WebBrowserSession } from 'navigation-stack'
171
+ // As soon as a page has been rendered, without any delay, tell `NavigationStack` to restore
172
+ // a previously-saved scroll position, if there's any.
173
+ //
174
+ // This method must be called both for the initial location and any subsequent location.
175
+ //
176
+ navigationStack.locationRendered(location)
177
+ }
216
178
 
217
- // Create a `NavigationStack` instance with a `maintainScrollPosition: true` option.
218
- const navigationStack = new NavigationStack(new WebBrowserSession(), {
219
- maintainScrollPosition: true
220
- })
179
+ // Subscribe to location changes.
180
+ navigationStack.subscribe(onLocationChange)
221
181
 
222
182
  //----------------------------------------------------------------------------------------
223
183
 
224
- // Sets the initial location.
184
+ // Start at the current location which is assumed to be "/initial-location".
185
+ // No argument when using `WebBrowserEnvironment`.
225
186
  navigationStack.init()
226
187
 
227
- // Render the initial location.
228
- document.body.innerHTML = '<div> Initial Location </div>'
229
-
230
- // As soon as a page has been rendered, without any delay, tell `NavigationStack` to restore
231
- // a previously-saved scroll position, if there's any.
232
- //
233
- // This method must be called both for the initial location and any subsequent location.
234
- //
235
- navigationStack.locationRendered()
236
-
237
- //----------------------------------------------------------------------------------------
238
-
239
- // Set the `location` to be a new location.
188
+ // Set the `location` to be "/new-location".
240
189
  //
241
190
  // This also updates the URL in the web browser's address bar
242
191
  // and adds a new entry in the web browser's navigation history.
243
192
  //
244
193
  navigationStack.push('/new-location')
245
194
 
246
- // Render the new location.
247
- document.body.innerHTML = '<div> New Location </div>'
248
-
249
- // The new location is now rendered.
250
- // Immediately after it has been rendered, call `.locationRenered()`.
251
- // There's no scroll position to restore because it's not a previously-visited location.
252
- navigationStack.locationRendered()
253
-
254
- //----------------------------------------------------------------------------------------
255
-
256
- // Set `location` "back" to the initial location.
195
+ // Set `location` "back" to "/initial-location".
257
196
  //
258
197
  // This also updates the URL in the web browser's address bar
259
198
  // and repositions the "current location" pointer in the web browser's navigation history.
260
199
  //
261
200
  navigationStack.shift(-1)
262
201
 
263
- // Render the initial location.
264
- document.body.innerHTML = '<div> Initial Location </div>'
265
-
266
- // The initial location is now rendered.
267
- // Immediately after it has been rendered, call `.locationRenered()`.
268
- // Restores the scroll position at the initial location.
269
- navigationStack.locationRendered()
270
-
271
202
  //----------------------------------------------------------------------------------------
272
203
 
273
204
  // (optional)
@@ -278,11 +209,61 @@ navigationStack.locationRendered()
278
209
  navigationStack.stop()
279
210
  ```
280
211
 
281
- `NavigationStack` provides methods:
212
+ `NavigationStack` constructor relevant options:
213
+
214
+ * `manageScrollPosition: true` — Enables the automatic scroll position management feature.
215
+ * `shouldChangePageScrollPositionOnLocationChange(prevLocation?, newLocation): boolean` — Decides whether page scroll position management should still be active for a given transition from `prevLocation` to `newLocation`. Is only relevant when `manageScrollPosition: true` option is passed to `NavigationStack` constructor. As the most obvious use case, it allows an application to selectively disable the effect of resetting page scroll position when replacing the URL with same pathname but different query parameters.
216
+
217
+
218
+ `NavigationStack` relevant methods:
219
+
220
+ * `addScrollableContainer(key: string, element: Element, options?: object)` — Use it in cases when it should restore not only the page scroll position but also the scroll position(s) of any other scrollable container(s). Returns a "remove scrollable container" function.
221
+ * `options` object could have properties:
222
+ * `shouldChangeScrollPositionOnLocationChange(prevLocation?, newLocation): boolean` — Decides whether scroll position management inside this scrollable container should still be active for a given transition from `prevLocation` to `newLocation`. Is only relevant when `manageScrollPosition: true` option is passed to `NavigationStack` constructor. As the most obvious use case, it allows an application to selectively disable the effect of resetting scroll position inside a scrollable container when replacing the URL with same pathname but different query parameters.
282
223
 
283
- * `addScrollableContainer(key: string, element: Element)` — Use it in cases when it should restore not only the page scroll position but also the scroll position(s) of any other scrollable container(s). Returns a "remove scrollable container" function.
284
224
  * `locationRendered()` — Call it every time a different location has been rendered, including the initial location, without any delay, i.e. immediately after a different location has been rendered.
285
225
 
226
+ By default, when restoring scroll position, it uses basic "immediate" scrolling. A developer could supply a custom `scrollPositionSetter` option with an implementation of custom scrolling behavior. For example, it could be some kind of "smooth" scrolling or something like that.
227
+
228
+ ```js
229
+ new NavigationStack(WebBrowserEnvironment, {
230
+ manageScrollPosition: true,
231
+ scrollPositionSetter: SmoothScrollPositionSetter
232
+ })
233
+
234
+ class SmoothScrollPositionSetter {
235
+ // Sets scroll position of a page or a scrollable element.
236
+ // Returns a `Promise` that resolves when it has finished setting the scroll position.
237
+ async set(
238
+ // `scrollableContainer: Element`.
239
+ // This is the scrollable container whose scroll position should be set.
240
+ // * When setting page scroll position, `scrollableContainer` is `undefined`.
241
+ // * When setting scrollable element scroll position, `scrollableContainer` is the scrollable element.
242
+ scrollableContainer,
243
+ // `scrollPositionOrAnchor: string | [number, number]`.
244
+ // This is the scroll position to set.
245
+ // * When setting page scroll position, it could be either an anchor or numeric coordinates.
246
+ // * When setting scrollable element scroll position, it could only be numeric coordinates.
247
+ scrollPositionOrAnchor,
248
+ // `scrollPosition` provides various "helper" methods for setting scroll position according to the environment.
249
+ // For example, in the context of a `WebBrowserEnvironment`, it provides the methods for setting scroll position in a web browser.
250
+ scrollPositionHelper
251
+ ) {
252
+ if (typeof scrollPositionOrAnchor === 'string') {
253
+ await smoothScrollToAnchor(scrollableContainer, scrollPositionOrAnchor)
254
+ } else {
255
+ await smoothScrollToCoordinates(scrollableContainer, scrollPositionOrAnchor)
256
+ }
257
+ }
258
+
259
+ // Cancels any pending (or in-progress) setting of scroll position.
260
+ cancel() {
261
+ stopSmoothScrolling()
262
+ }
263
+ }
264
+ ```
265
+
266
+ <!--
286
267
  <details>
287
268
  <summary>Using scroll position restoration feature without <code>NavigationStack</code></summary>
288
269
 
@@ -382,6 +363,7 @@ scrollPositionRestoration.stop()
382
363
  * `locationRendered(location)` — Call it every time a different location has been rendered, including the initial location, without any delay, i.e. immediately after a different location has been rendered. The location argument must have a `key`.
383
364
  * `stop()` — Stops scroll position restoration and clears any listeners or timers.
384
365
  </details>
366
+ -->
385
367
 
386
368
  ## Base Path
387
369
 
@@ -396,26 +378,26 @@ createMiddlewares(session, { basePath?: '/base-path' })
396
378
  If the web application is hosted under a certain URL prefix, it should be specified as a `basePath` parameter when creating a `NavigationStack` instance. This prefix will automatically be added to the URL in the web browser's address bar while the `location` object itself won't include it in the `pathname`.
397
379
 
398
380
  ```js
399
- new NavigationStack(new WebBrowserSession(), { basePath: '/base-path' })
381
+ new NavigationStack(WebBrowserEnvironment, { basePath: '/base-path' })
400
382
  ```
401
383
 
402
- ## Session
384
+ ## Environment
403
385
 
404
- A "session" ties `NavigationStack` to the environment it operates in, such as a web browser.
386
+ An "environment" class ties `NavigationStack` to the physical environment it operates in, such as a web browser.
405
387
 
406
- Three different "session" implementations are shipped with this package:
388
+ Three different "environment" implementations are shipped with this package:
407
389
 
408
- - Use `WebBrowserSession` in a web browser. Such session survives a page refresh and is automatically destroyed when the web browser tab gets closed.
409
- - Use `ServerSideRenderSession` in server-side rendering. Create a separate session for each incoming HTTP request. Initialize it with a relative URL of the HTTP request. If, during server-side render, the application code attempts to navigate to another location, it will throw a `ServerSideNavigationError` with a `location` property in it.
410
- - Use `InMemorySession` in tests to mimick a `WebBrowserSession`. Create a separate session for each separate navigation session. Initialize it with a relative URL or a location object.
390
+ - Use `WebBrowserEnvironment` in a web browser. Navigation session survives a page refresh and is only destroyed when the web browser tab gets closed. Create a single `NavigationStack` instance per web browser tab.
391
+ - Use `ServerSideRenderEnvironment` in server-side rendering. Create a separate `NavigationStack` instance for each incoming HTTP request. Initialize it with a relative URL of the HTTP request. If, during server-side render, the application code attempts to navigate to another location, it will throw a `ServerSideRedirectError` with a `location` property in it.
392
+ - Use `InMemoryEnvironment` in tests to mimick a `WebBrowserEnvironment`. One can create as many separate `NavigationStack` instances as required because they're completely independent/isolated from one another. Initialize it with a relative URL or a location object.
411
393
 
412
394
  <details>
413
- <summary>See <code>ServerSideRenderSession</code> example</summary>
395
+ <summary>See <code>ServerSideRenderEnvironment</code> example</summary>
414
396
 
415
397
  ######
416
398
 
417
399
  ```js
418
- const navigationStack = new NavigationStack(new ServerSideRenderSession())
400
+ const navigationStack = new NavigationStack(ServerSideRenderEnvironment)
419
401
 
420
402
  navigationStack.subscribe((location) => {
421
403
  console.log('Current location', location)
@@ -426,19 +408,19 @@ navigationStack.subscribe((location) => {
426
408
  navigationStack.init('/initial-location')
427
409
 
428
410
  // Navigates to a new location.
429
- // Throws `ServerSideNavigationError` with a `location` property.
411
+ // Throws `ServerSideRedirectError` with a `location` property.
430
412
  navigationStack.push('/new-location')
431
413
  ```
432
414
  </details>
433
415
 
434
416
 
435
417
  <details>
436
- <summary>See <code>InMemorySession</code> example</summary>
418
+ <summary>See <code>InMemoryEnvironment</code> example</summary>
437
419
 
438
420
  ######
439
421
 
440
422
  ```js
441
- const navigationStack = new NavigationStack(new InMemorySession())
423
+ const navigationStack = new NavigationStack(InMemoryEnvironment)
442
424
 
443
425
  navigationStack.subscribe((location) => {
444
426
  console.log('Current location', location)
@@ -456,8 +438,7 @@ navigationStack.push('/new-location')
456
438
 
457
439
  ######
458
440
 
459
- Every "session" has a unique `key`.
460
-
441
+ <!--
461
442
  Once created, a "session" is simply passed to the `NavigationStack` constructor and then you don't have to deal with it anymore — `NavigationStack` will pull all the strings for you.
462
443
 
463
444
  However, if someone prefers to completely bypass `NavigationStack` and interact with a "session" object directly, they could do so.
@@ -481,12 +462,13 @@ However, if someone prefers to completely bypass `NavigationStack` and interact
481
462
  * `delta: number` after a `.shift(delta)` navigation, i.e. "back or forward navigation".
482
463
  * `-1` after the user clicks a "Back" button in their web browser.
483
464
  * `1` after the user clicks a "Forward" button in their web browser.
484
- <!-- * `getInitialLocation(): object?` — Returns the initial location, if the session can get it from somewhere. For example, in a web browser, the initial location can be read from `window.location`. In other environments, such as server side, the initial location can't be read from anywhere. -->
465
+ <!- * `getInitialLocation(): object?` — Returns the initial location, if the session can get it from somewhere. For example, in a web browser, the initial location can be read from `window.location`. In other environments, such as server side, the initial location can't be read from anywhere. ->
485
466
  * `start(initialLocation?: object)` — Starts the session. The `initialLocation` argument is optional when the session can read it from somewhere. For example, `WebBrowserSession` can read `initialLocation` from `window.location`.
486
467
  * `stop()` — Stops the session. Cleans up any listeners, etc.
487
468
  * `navigate(operation: string, location: object)` — Navigates to a `location` using either `"PUSH"` or `"REPLACE"` operation. The `location` argument should be a result of calling `parseInputLocation()` function.
488
469
  * `shift(delta: number)` — Navigates "back" or "forward" by skipping a specified count of pages. Negative `delta` skips backwards, positive `delta` skips forward.
489
470
  </details>
471
+ -->
490
472
 
491
473
  ## Utility
492
474
 
@@ -549,25 +531,19 @@ removeBasePath({ pathname: '/base-path/abc' }, '/base-path') === { pathname: '/a
549
531
 
550
532
  ## Block Navigation
551
533
 
552
- `navigation-stack` provides the ability to block navigation. Call `addNavigationBlocker()` function to set up a "navigation blocker".
534
+ `NavigationStack` provides the ability to block navigation. Call `.addNavigationBlocker()` method to set up a "navigation blocker".
553
535
 
554
536
  ```js
555
537
  import {
556
538
  NavigationStack,
557
- WebBrowserSession,
558
- addNavigationBlocker
539
+ WebBrowserEnvironment
559
540
  } from 'navigation-stack'
560
541
 
561
- // Create a session.
562
- const session = new WebBrowserSession()
563
-
564
542
  // Create a `NavigationStack` instance.
565
- const navigationStack = new NavigationStack(session)
543
+ const navigationStack = new NavigationStack(WebBrowserEnvironment)
566
544
 
567
545
  // Add a navigation blocker.
568
- // It should be tied to the same "session".
569
- const removeNavigationBlocker = addNavigationBlocker(
570
- session,
546
+ const removeNavigationBlocker = navigationStack.addNavigationBlocker(
571
547
  (newLocation) => {
572
548
  // Returning `true` means "this navigation should be blocked".
573
549
  return true
@@ -594,188 +570,74 @@ navigationStack.push('/new-location')
594
570
 
595
571
  Navigation blocker should be a function that receives a `newLocation` argument and could be "synchronous" or "asynchronous" (i.e. return a `Promise`, aka `async`/`await`).
596
572
 
597
- The `newLocation` argument of a blocker function might not necessarily have a `key` or `index` property but other properties are present.
573
+ The `newLocation` argument of a blocker function is an object that has all the properties of a [standard web browser location](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) with the addition of a `query` object.
598
574
 
599
575
  Navigation blockers fire both when navigating from one page to another and when closing the current browser tab. In the latter case, `newLocation` argument will be `null`, and also the blocker function can't return a `Promise` (because it won't wait), and returning `true` from it will cause the web browser will to show a confirmation modal with a non-customizable generic browser-specific text like "Leave site? Changes you made might not be saved".
600
576
 
601
577
  ## Data Storage
602
578
 
603
- One could use `DataStorage` to store any kind of application-specific data in a given "session". The data will exist as long as the "session" exists.
579
+ One could use `NavigationStack`'s "data storage" to store any kind of application-specific data within the bounds of a given "session", which could be defined as the time from "opening" the application to "closing" it. As long as the "session" exists, so does the data in the "data storage".
604
580
 
605
- Different types of data could be stored under a different `key`.
581
+ For example, in a web browser environment, a "session" starts when the user opens a website in a web browser window or tab, and ends when the user closes that web browser window or tab, and such "session" also survives a "page refresh".
606
582
 
607
- If each different location should have it's own data stored under the same `key`, one could use `LocationDataStorage` instead of just `DataStorage`. For example, one could store scroll position for each different page to be able to restore it when the user decides to navigate "Back" to that page. By the way, that's precisely what `ScrollPositionRestoration` does.
583
+ Different types of data could be stored under a different `key`.
608
584
 
609
- `DataStorage` constructor receives a `session` argument and a `namespace` parameter. The `namespace` just gets prepended to every `key`. The idea is that your `namespace` must not clash with anyone else's `namespace` who might potentially use the same `session` to store their own data.
585
+ Each different location has it's own isolated data storage compartment, so the same `key` could be reused by different locations and there'd be no conflict. For example, one could store scroll position for each different page under `key: "scroll-position"` to be able to restore it when the user decides to navigate "Back" to that page. By the way, that's how `manageScrollPosition: true` feature works.
610
586
 
611
587
  ```js
612
- import { WebBrowserSession } from 'navigation-stack'
613
- import { DataStorage, LocationDataStorage } from 'navigation-stack/data-storage'
614
-
615
- const session = new WebBrowserSession()
616
-
617
- // `DataStorage` example
588
+ import { NavigationStack, WebBrowserEnvironment } from 'navigation-stack'
618
589
 
619
- const dataStorage = new DataStorage(session, { namespace: 'my-namespace' })
590
+ const navigationStack = new NavigationStack(WebBrowserEnvironment)
620
591
 
621
- dataStorage.set('key', 123)
622
- dataStorage.get('key') === 123
623
-
624
- // `LocationDataStorage` example
625
-
626
- const locationDataStorage = new LocationDataStorage(session, { namespace: 'my-namespace' })
592
+ navigationStack.init()
627
593
 
628
- const location = { pathname: '/abc' }
594
+ const location = navigationStack.current()
629
595
 
630
- locationDataStorage.set(location, 'key', 123)
631
- locationDataStorage.get(location, 'key') === 123
596
+ navigationStack.dataStorage.set(location, 'key', 123)
597
+ navigationStack.dataStorage.get(location, 'key') === 123
632
598
  ```
633
599
 
634
- `DataStorage` or `LocationDataStorage` don't provide any guarantees about actually storing the data: if it encounters any errors in the process, it simply ignores them. This simplifies the API in a way that the application doesn't have to wrap `.get()`/`.set()` calls in a `try/catch` block. And judging by the nature of location-specific data, that type of data is inherently non-essential and rather "nice-to-have".
635
-
636
- One might ask: Why use `DataStorage` or `LocationDataStorage` when one could simply store the data in a usual variable? The answer is that a usual variable doesn't survive if the user decides to refresh the page. But the entire navigation history does survive because that's how web browsers work. So if the user decides to go "Back" after refreshing the current page, the data associated to that previous location would already be lost and can't be recovered. In contrast, when using a `DataStorage` or `LocationDataStorage` with a `WebBrowserSession`, the stored data does survive a page refresh, which feels more consistent and coherent with the persistence behavior of the navigation history itself.
637
-
638
- ## Redux
639
-
640
- Under the hood, `navigation-stack` uses [`redux`](https://redux.js.org/). Why? For no particular reason. The original [`farce`](http://npmjs.com/package/farce) package was published in September 2016, and by that time `redux` had still been a hot topic since [July 2025](https://www.youtube.com/watch?v=xsSnOQynTHs). This package could most certainly be rewritten without using `redux`, it's just that there seems to be no need to do that.
641
-
642
- So since `navigation-stack` already implements all that `redux` stuff internally, such as "middlewares" or "actions", why not export it for public usage? Maybe there're still some `redux` fans out there.
643
-
644
- Using `navigation-stack` `redux`-way is equivalent to using it the conventional way via `NavigationStack` class. `navigation-stack` exports "middlewares", "actions" and a "reducer" that could be used in conjunction with `redux` or any other `redux`-compatible package (e.g. [`mini-redux`](https://www.npmjs.com/package/mini-redux)).
600
+ The data storage doesn't provide strict guarantees about actually storing the data: if it encounters an unexpected storage error in the process, it will simply ignore it. This simplifies the API in a way that the application doesn't have to wrap `.get()`/`.set()` calls in a `try/catch` block. And judging by the nature of location-specific data, that type of data is inherently non-essential (non-critical) and rather "nice-to-have".
645
601
 
646
602
  <details>
647
- <summary>See <code>redux</code>-style API</summary>
603
+ <summary>Examples of ignored errors in a <code>WebBrowserEnvironment</code>.</summary>
648
604
 
649
605
  ######
650
606
 
651
- Start by creating a Redux "store" with `navigation-stack` middlewares.
652
-
653
- ```js
654
- import { createStore, applyMiddleware } from 'redux';
655
-
656
- import {
657
- createMiddlewares,
658
- locationReducer,
659
- Actions,
660
- WebBrowserSession,
661
- } from 'navigation-stack';
662
-
663
- // Create a Redux store.
664
- const store = createStore(
665
- // Reducer function. For example, `locationReducer()`.
666
- locationReducer,
667
- // It should be tied to a navigation "session".
668
- applyMiddleware(...createMiddlewares(new WebBrowserSession())),
669
- );
670
- ```
607
+ * `SecurityError` In "Private"/"Incognito" browsing mode, many browsers block write access to storage APIs to enhance privacy. Attempting to call `sessionStorage.setItem()` will throw a `SecurityError` in such case. Same error could be a result of using a really strict privacy blocker extension or opening the website from an `*.html` file directly from disk (`file://` URL).
671
608
 
672
- Next, set the initial location. Normally, `NavigationStack` class API automatically performs this step for a developer. When using Redux API though, it doesn't do that and the developer has to do it themself.
609
+ * `QuotaExceededError` — Could happen if the application attempts to store too much data in `navigation-stack`'s "data storage", or if the application has already used up all available space in `sessionStorage` for some other purposes. The maximum available space in `sessionStorage` depends on the web browser and is usually assumed to be around `5 MB` per URL origin.
610
+ </details>
673
611
 
674
- ```js
675
- // Sets the initial `location`.
676
- //
677
- // Accepts either a relative URL string or a location object.
678
- //
679
- // The initial location argument could be omitted for `WebBrowserSession`
680
- // because it can read it by itself from `window.location`.
681
- // Other types of session such as `InMemorySession` or `ServerSideRenderSession`
682
- // don't have an initial location and require the initial location argument
683
- // to be specified explicitly when creating an `Actions.init(initialLocation)` action.
684
- //
685
- store.dispatch(Actions.init());
686
- ```
612
+ ######
687
613
 
688
- Then subscribe to location changes. One could use Redux'es standard [subscription mechanisms](https://redux.js.org/api/store#subscribelistener) to immediately get notified of current location changes.
614
+ One might ask: Why use `NavigationStack`'s data storage when one could simply store the data in a usual variable? The answer is that a usual variable doesn't survive if the user decides to refresh the page. But the entire navigation history does survive because that's how web browsers work. So if the user decides to go "Back" after refreshing the current page, the data associated to that previous location would already be lost and can't be recovered. In contrast, when using `NavigationStack` with a `WebBrowserEnvironment`, the stored data does survive a page refresh, which feels more consistent and coherent with the persistence behavior of the navigation history itself.
689
615
 
690
- ```js
691
- let currentLocation;
616
+ ## Development
692
617
 
693
- // Create a Redux store.
694
- const store = createStore(
695
- locationReducer, // Reducer function. For example, `locationReducer()`.
696
- applyMiddleware(...createMiddlewares(new WebBrowserSession())),
697
- );
618
+ Clone the repository. Then:
698
619
 
699
- // Subscribe to any potential Redux state changes.
700
- const unsubscribe = store.subscribe(() => {
701
- const previousLocation = currentLocation;
702
- currentLocation = store.getState(); // In case of using `locationReducer()`.
703
- if (currentLocation !== previousLocation) {
704
- console.log('Current location', currentLocation);
705
- }
706
- });
707
620
  ```
708
-
709
- Now ready to perform navigation actions by dispatching any of the available `Actions`.
710
-
711
- ```js
712
- // Sets the `location` to be a new location.
713
- //
714
- // Also updates the URL in the web browser's address bar.
715
- //
716
- // Also adds a new entry in the web browser's navigation history.
717
- //
718
- store.dispatch(Actions.push('/new-location'));
719
-
720
- // Sets the `location` to be a new location.
721
- //
722
- // Also updates the URL in the web browser's address bar.
723
- //
724
- // Does not add a new entry in the web browser's navigation history
725
- // which is the only difference between this and `Actions.push()`.
726
- //
727
- store.dispatch(Actions.replace('/new-location'));
728
-
729
- // Sets the `location` to be a previous one (if there is one).
730
- // One could think of it as an equivalent of clicking a "Back" button in a web browser.
731
- //
732
- // Also updates the URL in the web browser's address bar.
733
- //
734
- // Also shifts the current position in the web browser's navigation history.
735
- //
736
- store.dispatch(Actions.shift(-1));
737
-
738
- // Sets the `location` to be a next one (if there is one).
739
- // One could think of it as an equivalent of clicking a "Forward" button in a web browser.
740
- //
741
- // Also updates the URL in the web browser's address bar.
742
- //
743
- // Also shifts the current position in the web browser's navigation history.
744
- //
745
- store.dispatch(Actions.shift(1));
621
+ yarn
622
+ yarn format
623
+ yarn test
746
624
  ```
747
625
 
748
- To get the current location:
626
+ It will open two web browser windows — Firefox and Chrome — and run live tests in those. The web browsers are specified in `karma.conf.cjs` file. When running tests, don't unfocus the web browser windows, otherwise the tests will fail with random errors. If you're not unfocusing the web browser windows and the tests still fail with random errors, see if increasing the interval in `await delay(100)` calls in tests fixes the issue.
749
627
 
750
- ```js
751
- // When `locationReducer()` is used, `store.getState()` returns the current location.
752
- const location = store.getState();
753
- console.log(location);
754
- ```
628
+ ## Development History
755
629
 
756
- (optional) After the user is done using the app, stop the session and clean up any listeners.
630
+ Originally it started from a fork of [`farce`](http://npmjs.com/package/farce) package to fix a couple of small bugs there ([1](https://github.com/4Catalyzer/farce/issues/483), [2](https://github.com/4Catalyzer/farce/issues/491)).
757
631
 
758
- ```js
759
- // (optional)
760
- // When the user closes the application,
761
- // stop the session and clean up any listeners.
762
- // There's no need to do this in a web browser.
763
- unsubscribe();
764
- store.dispatch(Actions.stop());
765
- ```
766
- </details>
632
+ Then I decided to merge it with [`scroll-behavior`](http://npmjs.com/package/scroll-behavior) package to fix a couple of small bugs there ([1](https://github.com/taion/scroll-behavior/issues/215), [2](https://github.com/taion/scroll-behavior/pull/472)).
767
633
 
768
- ## Development
634
+ Then I decided to completely reorganize and refactor the entire code.
769
635
 
770
- Clone the repository. Then:
636
+ Then I decided to remove Redux and expose a more conventional and simple API. The original [`farce`](http://npmjs.com/package/farce) package was published in September 2016, and by that time `redux` had [still been](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367) a hot topic since [July 2015](https://www.youtube.com/watch?v=xsSnOQynTHs). But in retrospect, there were no legitimate reasons to heavily rely on Redux when implementing such a small and universal library. Apparently, in 2016, everyone went crazy over Redux and it became a de-facto standard when building just about any React web application, to the point of assuming that if you're building a React web app, you're 100% building it on Redux, so all hot frameworks should reuse that Redux for both the internal implementation and the public API.
771
637
 
772
- ```
773
- yarn
774
- yarn format
775
- yarn test
776
- ```
638
+ > Redux was a revolutionary technology in the React ecosystem. It enabled us to have a global store with [immutable data](https://medium.com/dailyjs/the-state-of-immutability-169d2cd11310) and **fixed the issue of [prop-drilling](https://kentcdodds.com/blog/prop-drilling) in our component tree. For sharing immutable data across an application, it continues to be an excellent tool that scales really well.
777
639
 
778
- It runs tests in two web browsers (for no particular reason) — Chrome and Firefox (configurable in `karma.conf.cjs`). When running `yarn test`, it opens Chome and Firefox browser windows. Don't unfocus those windows, otherwise the tests won't finish.
640
+ Source: [Why I Stopped Using Redux](https://dev.to/g_abud/why-i-quit-redux-1knl)
779
641
 
780
642
  ## GitHub
781
643
 
package/karma.conf.cjs CHANGED
@@ -75,6 +75,6 @@ module.exports = (config) => {
75
75
  },
76
76
  },
77
77
 
78
- browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome', 'Firefox'],
78
+ browsers: env.BROWSER ? env.BROWSER.split(',') : ['Firefox', 'Chrome'],
79
79
  });
80
80
  };