|
| 1 | +# ADR: Unified client side routing in Juno |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +The application requires robust client-side routing with support for: |
| 6 | + |
| 7 | +- Configurable mounting base paths so application router can work with a parent router in the shell app. |
| 8 | +- Optional hashed routing in case shell app don't want to mount child app on certain path or does not have client side routing at all. |
| 9 | +- Data preloading and caching. |
| 10 | +- URL-based state persistence. |
| 11 | +- Support for optional, UI-level state (e.g., modals or panels) without affecting navigation hierarchy. |
| 12 | + |
| 13 | +We evaluated several routing libraries including React Router and TanStack Router. After evaluating feature sets, flexibility, and community support, we decided to adopt **TanStack Router**. |
| 14 | + |
| 15 | +Additionally, to integrate routing with data fetching and ensure URL-driven state management, we considered various combinations of client-side state and URL parsing utilities. |
| 16 | + |
| 17 | +## Decision |
| 18 | + |
| 19 | +We adopted the following architecture for client-side routing and state: |
| 20 | + |
| 21 | +1. **Routing Library**: We use **TanStack Router** to manage client-side routing. |
| 22 | + |
| 23 | + - The router mounts to a `basePath`, provided via application props, defaulting to `'/'`. |
| 24 | + - This allows embedding the app under sub-paths without requiring changes to the router configuration. |
| 25 | + |
| 26 | +2. **Hashed Routing Support**: |
| 27 | + |
| 28 | + - The router can optionally use hashed history (`#` URLs) when the prop `enableHashedRouting` is set to `true`. It defaults to `false`. |
| 29 | + - This supports deployments where traditional URL rewriting is not available (e.g., GitHub Pages or an External Dashboard). |
| 30 | + |
| 31 | + - > **Note:** |
| 32 | + > TanStack Router has a known [issue/behavior](https://github.com/TanStack/router/issues/4370#issuecomment-3012344925) in **hashed routing mode** where it includes query parameters from the entire URL—not just the hash fragment—when supplying the `searchString`. |
| 33 | + > If your application depends on extracting query parameters specifically from the **hash fragment**, you’ll need to handle this manually in the `parseSearch` method within your router configuration. |
| 34 | +
|
| 35 | +3. **Data Loading and Caching**: |
| 36 | + |
| 37 | + - We use **TanStack Query** for invoking API calls and caching their results. |
| 38 | + - Apollo Client is still used for the actual GraphQL operations, but TanStack Query provides better control over caching and fetching behavior during route transitions. |
| 39 | + |
| 40 | +4. **URL State Encoding/Decoding**: |
| 41 | + |
| 42 | + - We utilize the `v2/encode` and `v2/decode` utilities from the `url-state-provider` package to handle query string parameters. For more information see the section below [Why we use `v2/encode` and `v2/decode` from url-state-provider](#why-we-use-v2encode-and-v2decode-from-url-state-provider). |
| 43 | + - Applications may persist their state in the URL, as long as: |
| 44 | + - The state can be serialized by the `encode` utility. |
| 45 | + - The state can be correctly interpreted back after `decode` utility is applied. |
| 46 | + |
| 47 | +5. **Route Definitions and Optional UI State**: |
| 48 | + |
| 49 | + - Routes are defined using TanStack Router's file-based routing to reflect the app’s core navigational paths. |
| 50 | + - To control **optional UI elements** such as panels, modals, or drawers (that can appear conditionally on a given page), we use **URL search parameters** rather than path parameters. |
| 51 | + - This ensures that opening or closing optional UI components does **not trigger full route transitions** or reload data unnecessarily. |
| 52 | + - It also preserves the logical page hierarchy and improves back/forward navigation behavior in the browser. |
| 53 | + |
| 54 | + #### Examples |
| 55 | + |
| 56 | + Following are the URLs created from following file structure |
| 57 | + `/products -> index.html` |
| 58 | + |
| 59 | + ##### Basic Route |
| 60 | + |
| 61 | + - `/products` |
| 62 | + Loads the main **Products** page. |
| 63 | + |
| 64 | + - `/products/product-a` |
| 65 | + Loads the page for **Product A** |
| 66 | + |
| 67 | + ##### Optional UI Elements via Search Params |
| 68 | + |
| 69 | + - `/products?overview=product-a` |
| 70 | + Shows quick overview of the production in the form of, say, panel. |
| 71 | + |
| 72 | + - `/products?sort=desc` |
| 73 | + Sorts list of products in descending order. |
| 74 | + |
| 75 | + - `/products?f_availability=germany,uk` |
| 76 | + Applies availability filter where a product is either available in Germany or UK |
| 77 | + |
| 78 | +## Consequences |
| 79 | + |
| 80 | +- **Pros**: |
| 81 | + |
| 82 | + - Flexible routing that supports multiple deployment environments, Shell App(with or without router), External Dashboards. |
| 83 | + - Improved control over API call timing and caching via TanStack Query. |
| 84 | + - Clear separation of concerns between routing, data loading, and state persistence. |
| 85 | + - Clean URL patterns that distinguish between core navigation and optional UI state. |
| 86 | + - Enhanced browser UX with predictable back/forward behavior for panels and modals. |
| 87 | + |
| 88 | +- **Cons**: |
| 89 | + - Increases the learning curve slightly due to the use of newer libraries (TanStack Router). |
| 90 | + - The encode/decode contract must be strictly followed to prevent inconsistent behavior. |
| 91 | + |
| 92 | +## Why we use `v2/encode` and `v2/decode` from url-state-provider |
| 93 | + |
| 94 | +Recently, we introduced v2 of the encoding/decoding utilities in `url-state-provider`. These utilities are essentially thin wrappers around the `query-string` library (which is also referenced in the official TanStack Router docs). By default, TanStack uses `JSON.stringify` for URL serialization. |
| 95 | + |
| 96 | +### Example: URL Search Params Serialization |
| 97 | + |
| 98 | +Consider the following state object: |
| 99 | + |
| 100 | +```js |
| 101 | +{ |
| 102 | + filter: ['active', 'pending'], |
| 103 | + sort: 'date', |
| 104 | + page: 2 |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +**Serialized with `query-string` (used by `v2/encode`):** |
| 109 | + |
| 110 | +``` |
| 111 | +/dashboard?filter=active,pending&sort=date&page=2 |
| 112 | +``` |
| 113 | + |
| 114 | +**Serialized with `JSON.stringify` (TanStack Router default):** |
| 115 | + |
| 116 | +``` |
| 117 | +/dashboard?state=%7B%22filter%22%3A%5B%22active%22%2C%22pending%22%5D%2C%22sort%22%3A%22date%22%2C%22page%22%3A2%7D |
| 118 | +``` |
| 119 | + |
| 120 | +> The `query-string` approach produces a flatter, more human-readable URL, while `JSON.stringify` encodes the entire state as a single opaque string. |
| 121 | +
|
| 122 | +You might wonder: If we can use `query-string` directly with TanStack Router, why expose it through `url-state-provider`? |
| 123 | + |
| 124 | +Here are the key reasons: |
| 125 | + |
| 126 | +- **Consistent flat structure:** We want application developers to persist state to the URL in a flat format—not deeply nested. Arrays of primitive values are allowed, but complex nested collections are not. |
| 127 | +- **Improved readability:** The `query-string` library provides a more human-readable URL format compared to the default `JSON.stringify` output. For example, we can put arrays of strings in a nicely comma-separated form in the URL, which is human readable (filters are a very good use-case of it). |
| 128 | +- **Centralized control:** Exposing the utility through `url-state-provider` lets us control the `query-string` version and apply sensible, consistent defaults across the app. |
| 129 | + |
| 130 | +By using `v2/encode` and `v2/decode`, we ensure that our application's URL state is robust, predictable, and easy to maintain, even as requirements evolve. |
| 131 | + |
| 132 | +## Notes |
| 133 | + |
| 134 | +- Developers must validate state objects before encoding to prevent invalid URL states. |
0 commit comments