# State in URL (deep links)

*Category:* Getting started
*Section:* Guidelines
*Source:* https://uikit.developers.rio.cloud/#/start/guidelines/state-in-url
*Captured:* 2026-04-27T14:57:05.030Z

Every service frontend should support deep linking and reflect meaningful UI state in the URL. This enables restoring state when links are shared, when they are stored as bookmarks, when users navigate with browser history, or when a service is opened from another app.

## State in URL (deep links)

## Mental model

Think of the URL as the external representation of app state. It should describe where the user is and what they are looking at (intent), not what they are currently doing (interaction).

State that only lives in memory breaks a core web expectation. If users refresh or navigate back and lose filters, view settings, or selections, the app feels unreliable. URLs should preserve context so users can return to the same place without redoing their work.

## Decision criteria

Before putting something in the URL, ask:

- Would the user expect this to persist after reload or sharing the link?
- Would the link feel “broken” without it?
- Does it describe intent rather than a transient UI interaction?

## What belongs in the URL

## Good candidates

- Search queries and filters
- Sorting and column visibility (when it changes what users see)
- View modes (list/grid/table/map)
- Date ranges and time periods
- Selected items or active tabs (focus)
- UI configuration that affects content (e.g. grouping, expanded views)
- Feature variants (only if stable and safe to share)

Search queries and filters

Sorting and column visibility (when it changes what users see)

View modes (list/grid/table/map)

Date ranges and time periods

Selected items or active tabs (focus)

UI configuration that affects content (e.g. grouping, expanded views)

Feature variants (only if stable and safe to share)

## Poor candidates

- Sensitive information (passwords, tokens, PII) - NEVER!
- Temporary UI states (e.g. dropdown expanded, tooltip open)
- Form input in progress (unsaved changes)
- Very large or complex nested data
- Base64 encoded data
- High-frequency transient state (mouse position, scroll)

Sensitive information (passwords, tokens, PII) - NEVER!

Temporary UI states (e.g. dropdown expanded, tooltip open)

Form input in progress (unsaved changes)

Very large or complex nested data

Base64 encoded data

High-frequency transient state (mouse position, scroll)

## Example deep links

Prefer readable, self-describing URLs. Use consistent naming across services to make deep links easier to share and reason about.

## Annotated example

https://example.com/#/assets?q=truck&sort=-name&viewType=table&selectedAssetId=asset-123

q                search query (user intent)
sort             sort key with direction prefix ("-name" = descending by name)
viewType         content representation mode
selectedAssetId  focused entity that should be shareable/restorable

## More examples

https://example.com/#/overview?view=history&assetIds=5a88af1e-2fb2-494e-9240-6a833d5f1791&q=10&startDate=2019-06-17T00:00:00Z&endDate=2019-06-17T23:59:59Z&sort=timestamp#details
https://example.com/#/drivers/74eb88e1-1a84-4abe-90bc-9e0079807d57?driverState=ACTIVE&q=my%20test
https://example.com/#/users/0848-b02e-47be-bd77-d332f61a90d6?sort=-lastName
https://example.com/#/issues?projectKey=FM&view=details&selectedIssue=FM-4346

Treat the URL as user input. Invalid or unknown params should not break rendering. Fall back to defaults and keep the UI usable.

## Common param names

Use consistent, concise camelCase param names across services whenever possible. If URLs become verbose, prefer a well-known short form (e.g. q, lat / lng) and document it so other teams can align.

| Name | Description |
| --- | --- |
| assetIds | Comma-separated list of selected assets (stable IDs). |
| q | Search query (encode with encodeURIComponent). This short form is common in many search engines. No need to use verbose keys here. |
| sort | Sort key; use key / -key for direction. Prefer stable keys over display labels. |
| lat / lng | Latitude / longitude in decimal degrees. Avoid unnecessary precision. |
| z | Zoom level (integer). |
| startDate / endDate | Start and end timestamps. Prefer ISO formats and document timezone expectations (recommend UTC). |
| selectedAssetId / selectedIssue | Focused item in list/detail context. Prefer explicit names over generic placeholders. |
| viewType | Display mode (table/grid/map). Use a limited set of values. |

## URLs as contracts

Well-designed URLs are more than a storage place for state. They become a contract between your app and its consumers, setting expectations for humans, developers, and machines.

A clear URL draws boundaries between public and private state, shareable and session-only data, and client versus server responsibility. Developers know what is safe to persist, users know what they can bookmark, and machines know what is worth indexing.

Readable URLs communicate meaning. Compare:

// bad: unclear intent, hard to reason about
https://example.com/#/p?id=x7f2k&v=3

// good: self-describing, stable, easier to maintain
https://example.com/#/products/laptop?color=silver&sort=price

The first hides intent; the second explains itself and is easier to evolve without breaking consumers.

## How to

Typical flow:

- Define which parts of state belong in the URL and what the defaults are.
- Parse URL to initial state on load.
- Update URL when state changes (omit defaults to keep URLs compact).
- Choose history behavior (push vs replace) based on user expectations.

Use UIKIT utilities instead of hand-rolled parsers:

```ts
import { useLocation, useNavigate, useNavigationType } from 'react-router-dom';
import useUrlState from '@rio-cloud/rio-uikit/useUrlState';
import type { UrlConfigItem } from '@rio-cloud/rio-uikit/routeUtils';

type ViewType = 'list' | 'grid';
type AppState = { query: string; viewType: ViewType; tags: string[] };

const actions = {
setQuery: (value: string) => ({ type: 'setQuery' as const, payload: value }),
setViewType: (value: ViewType) => ({ type: 'setViewType' as const, payload: value }),
setTags: (value: string[]) => ({ type: 'setTags' as const, payload: value }),
};

const config: readonly UrlConfigItem<AppState, unknown>[] = [
{ key: 'q', selector: s => s.query, action: actions.setQuery, defaultValue: '' },
{ key: 'viewType', selector: s => s.viewType, action: actions.setViewType, defaultValue: 'list' },
{ key: 'tags', selector: s => s.tags, action: actions.setTags, type: 'array', defaultValue: [] },
];

const location = useLocation();
const navigate = useNavigate();
const navigationType = useNavigationType();

useUrlState({
config,
searchString: location.search,
storeValues: { q: state.query, viewType: state.viewType, tags: state.tags },
navigationType,
onUpdateStore: action => dispatch(action),
onNavigate: (search, mode) => navigate({ search }, { replace: mode === 'replace' }),
});
```

import

{

,

,

}

from

'react-router-dom'

;

import

from

'@rio-cloud/rio-uikit/useUrlState'

;

import

type

{

}

from

'@rio-cloud/rio-uikit/routeUtils'

;

type

ViewType

=

'list'

|

'grid'

;

type

AppState

=

{

:

string

;

:

;

:

string

[

]

}

;

const

=

{

setQuery

:

(

:

string

)

=>

(

{

:

'setQuery'

as

const

,

:

}

)

,

setViewType

:

(

:

)

=>

(

{

:

'setViewType'

as

const

,

:

}

)

,

setTags

:

(

:

string

[

]

)

=>

(

{

:

'setTags'

as

const

,

:

}

)

,

}

;

const

:

readonly

<

,

unknown

>

[

]

=

[

{

:

'q'

,

selector

:

=>

.

,

:

.

,

:

''

}

,

{

:

'viewType'

,

selector

:

=>

.

,

:

.

,

:

'list'

}

,

{

:

'tags'

,

selector

:

=>

.

,

:

.

,

:

'array'

,

:

[

]

}

,

]

;

const

=

useLocation

(

)

;

const

=

useNavigate

(

)

;

const

=

useNavigationType

(

)

;

useUrlState

(

{

,

:

.

,

:

{

:

.

,

:

.

,

:

.

}

,

,

onUpdateStore

:

=>

dispatch

(

)

,

onNavigate

:

(

,

)

=>

navigate

(

{

}

,

{

:

===

'replace'

}

)

,

}

)

;

> Implementation details: See useUrlState and route utils for details.

## Best practices

Keep URLs tidy, predictable, and easy to share. Compact URLs are easier to read, easier to compare, and less likely to break in older environments.

Handle defaults by omitting them from the URL. Only include values that differ from the default state.

// avoid: defaults add noise
?viewType=table&lang=en&sort=date&filters=type

// prefer: only non-default values
?viewType=grid

## pushState vs. replaceState

Use pushState when a change is a meaningful navigation step and should be undoable via the Back button (switching view type, applying a filter set). Use replaceState for small refinements where history noise would harm usability (search-as-you-type, incremental tweaks).

// use pushState for meaningful navigation steps (undoable via Back button)
?viewType=grid
?selectedAssetId=asset-123
?filters=type:truck,status:active

// use replaceState for refinements (avoid noisy history)
?q=tr
?q=tru
?q=truck

Respect the Back button: if a change should be undoable, use pushState. If it is just a refinement, prefer replaceState.

## URL state and Redux

Pick one source of truth and keep synchronization one-way to avoid loops.

**URL as source of truth (simple cases)**

- Read URL params directly (for example with React Router search params).
- Derive rendered state from URL values.
- Update URL directly from UI actions with push/replace based on intent.

```ts
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';

type ViewType = 'list' | 'grid';

// URL as source of truth (simple screens, no centralized store sync)
const [searchParams, setSearchParams] = useSearchParams();

const urlState = useMemo(
() => ({
q: searchParams.get('q') ?? '',
viewType: (searchParams.get('viewType') as ViewType) ?? 'list',
}),
[searchParams]
);

const setUrlState = (patch: Partial<typeof urlState>, historyMode: 'push' | 'replace' = 'push') => {
const next = new URLSearchParams(searchParams);
const merged = { ...urlState, ...patch };

if (merged.q) next.set('q', merged.q);
else next.delete('q');

if (merged.viewType === 'list') next.delete('viewType');
else next.set('viewType', merged.viewType);

setSearchParams(next, { replace: historyMode === 'replace' });
};

// Typical usage:
const onSearchChange = (q: string) => setUrlState({ q }, 'replace'); // typing/search refinement
const onViewTypeChange = (viewType: ViewType) => setUrlState({ viewType }, 'push'); // meaningful step
```

import

{

}

from

'react'

;

import

{

}

from

'react-router-dom'

;

type

ViewType

=

'list'

|

'grid'

;

// URL as source of truth (simple screens, no centralized store sync)

const

[

,

]

=

useSearchParams

(

)

;

const

=

useMemo

(

(

)

=>

(

{

:

.

get

(

'q'

)

??

''

,

:

(

.

get

(

'viewType'

)

as

)

??

'list'

,

}

)

,

[

]

)

;

const

=

(

:

<

typeof

>

,

:

'push'

|

'replace'

=

'push'

)

=>

{

const

=

new

URLSearchParams

(

)

;

const

=

{

...

,

...

}

;

if

(

.

)

.

set

(

'q'

,

.

)

;

else

.

delete

(

'q'

)

;

if

(

.

===

'list'

)

.

delete

(

'viewType'

)

;

else

.

set

(

'viewType'

,

.

)

;

setSearchParams

(

,

{

:

===

'replace'

}

)

;

}

;

// Typical usage:

const

onSearchChange

=

(

:

string

)

=>

setUrlState

(

{

}

,

'replace'

)

;

// typing/search refinement

const

onViewTypeChange

=

(

:

)

=>

setUrlState

(

{

}

,

'push'

)

;

// meaningful step

**Redux as source of truth (more complex but recommend for more complex SPAs)**

- Parse the URL once to seed initial Redux state.
- Subscribe to Redux and update the URL (debounced).
- Listen for URL changes and dispatch to Redux with guards to avoid loops and no-op updates.

```ts
import useUrlState from '@rio-cloud/rio-uikit/useUrlState';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useNavigate, useNavigationType } from 'react-router-dom';
import type { AppDispatch } from '../../../../setup/store';

// Redux as source of truth (recommended for larger SPAs)
// 1) Parse URL and dispatch updates on URL changes (including browser POP).
// 2) Observe store values and write URL changes via push/replace rules.
// 3) Keep this logic centralized in one component near the router.

const dispatch = useDispatch<AppDispatch>();
const location = useLocation();
const navigate = useNavigate();
const navigationType = useNavigationType();

const storeValues = useSelector(state => ({
q: getSearchValue(state),
sort: { sortBy: getSortBy(state), sortDir: getSortDir(state) },
filters: getActivityFilter(state),
}));

useUrlState({
config: tableUrlConfig,
searchString: location.search,
storeValues,
localStorageKey: 'service.ui.state',
navigationType,
onUpdateStore: action => dispatch(action),
onNavigate: (search, mode) => navigate({ search }, { replace: mode === 'replace' }),
});
```

import

from

'@rio-cloud/rio-uikit/useUrlState'

;

import

{

,

}

from

'react-redux'

;

import

{

,

,

}

from

'react-router-dom'

;

import

type

{

}

from

'../../../../setup/store'

;

// Redux as source of truth (recommended for larger SPAs)

// 1) Parse URL and dispatch updates on URL changes (including browser POP).

// 2) Observe store values and write URL changes via push/replace rules.

// 3) Keep this logic centralized in one component near the router.

const

=

useDispatch<AppDispatch>

(

)

;

const

=

useLocation

(

)

;

const

=

useNavigate

(

)

;

const

=

useNavigationType

(

)

;

const

=

useSelector

(

=>

(

{

:

getSearchValue

(

)

,

:

{

:

getSortBy

(

)

,

:

getSortDir

(

)

}

,

:

getActivityFilter

(

)

,

}

)

)

;

useUrlState

(

{

:

,

:

.

,

,

:

'service.ui.state'

,

,

onUpdateStore

:

=>

dispatch

(

)

,

onNavigate

:

(

,

)

=>

navigate

(

{

}

,

{

:

===

'replace'

}

)

,

}

)

;

useUrlState supports this second model: centralized store-url synchronization. If you still use template helpers like routeHelper with qs, replace them with a centralized config + useUrlState synchronizer and define per-field history behavior with history: 'push' | 'replace'.

## Things to consider

- Use comma-separated values for lists (keep them readable, avoid encoding commas in lists).
- Keep param names explicit and avoid merging unrelated state into one param.
- If a value equals the default, omit it from the URL to reduce clutter.
- Encode user input (search strings) with encodeURIComponent.
- Document formats for dates and timezones (recommend UTC) and coordinate precision.
- Treat URL params as untrusted input: ignore unknown params, fall back to defaults for invalid values, and never break rendering due to a malformed URL.

## Limitations

URL length limits vary by browser, OS, and environment. Modern browsers typically handle very long URLs, but older/legacy browsers can fail around a few thousand characters.

Keep URLs concise. If you need to support older environments, validate URL length limits in your target setup and consider alternative persistence (e.g. server-side saved views) for large state.