# useUrlState

*Category:* Utilities
*Section:* UI state & input hooks
*Source:* https://uikit.developers.rio.cloud/#/utilities/useUrlState
*Captured:* 2026-04-27T14:59:44.732Z

The useUrlState hook keeps store state and URL search params in sync. Define a config once and use the hook to read URL state into the store and write store changes back to the URL.

## useUrlState

This example uses React Router (useLocation, useNavigate, useNavigationType), but the hook also works without it.

In this demo, useReducer is used as a lightweight stand-in for a Redux store.

Prefer a single useUrlState instance as the writer and share its state via props or context. Multiple components writing independently can overwrite each other’s URL updates.

Each UrlConfigItem requires an explicit defaultValue so removed URL params can reliably reset state.

History behavior: history: 'push' always creates a new entry. history: 'replace' replaces updates, but still pushes once when a value changes from default to non-default, so users can navigate back to the pre-edit state.

> Further information For guidance on what should live in URLs, see the State in URL guideline. For route-specific parsing examples, visit the Route utils page.

### Example: Tags (picklist)

URL
Search query

View mode list grid
Amount (default: 1)
Tags (picklist)
Banana
Peach
Pear
Strawberry
Show archived
(default: "false" - will be omitted from the URL)
Current URL state (stored in my-service/#)
{
"query": "",
"view": "list",
"amount": 1,
"tags": [],
"showArchived": false
}

#### React (tsx)

```tsx
import { useMemo, useReducer } from 'react';
import { useLocation, useNavigate, useNavigationType } from 'react-router-dom';

import useUrlState from '@rio-cloud/rio-uikit/useUrlState';
import LabeledElement from '@rio-cloud/rio-uikit/LabeledElement';
import Card from '@rio-cloud/rio-uikit/Card';
import Checkbox from '@rio-cloud/rio-uikit/Checkbox';
import ClearableInput from '@rio-cloud/rio-uikit/ClearableInput';
import type { UrlConfigItem } from '@rio-cloud/rio-uikit/routeUtils';

const BASE_PATH = 'my-service/#';

const VIEW_OPTIONS = ['list', 'grid'] as const;

type ViewType = (typeof VIEW_OPTIONS)[number];
const DEFAULT_VIEW: ViewType = 'list';
const DEFAULT_AMOUNT = 1;

// ---------------------------------------------------------------------------
// State handling
// ---------------------------------------------------------------------------
type DemoState = {
    query: string;
    view: ViewType;
    amount: number;
    tags: string[];
    showArchived: boolean;
};

const initialState: DemoState = {
    query: '',
    view: DEFAULT_VIEW,
    amount: DEFAULT_AMOUNT,
    tags: [],
    showArchived: false,
};

const actions = {
    setQuery: (value: string) => ({ type: 'setQuery' as const, payload: value }),
    setView: (value: ViewType) => ({ type: 'setView' as const, payload: value }),
    setAmount: (value: number) => ({ type: 'setAmount' as const, payload: value }),
    setTags: (value: string[]) => ({ type: 'setTags' as const, payload: value }),
    setShowArchived: (value: boolean) => ({ type: 'setShowArchived' as const, payload: value }),
};

type DemoAction = ReturnType<(typeof actions)[keyof typeof actions]>;
type DemoConfigItem =
    | UrlConfigItem<DemoState, string>
    | UrlConfigItem<DemoState, ViewType>
    | UrlConfigItem<DemoState, number>
    | UrlConfigItem<DemoState, string[]>
    | UrlConfigItem<DemoState, boolean>;

const reducer = (state: DemoState, action: DemoAction): DemoState => {
    switch (action.type) {
        case 'setQuery':
            return { ...state, query: action.payload };
        case 'setView':
            return { ...state, view: action.payload };
        case 'setAmount':
            return { ...state, amount: action.payload };
        case 'setTags':
            return { ...state, tags: action.payload };
        case 'setShowArchived':
            return { ...state, showArchived: action.payload };
        default:
            return state;
    }
};

// ---------------------------------------------------------------------------
// URL configuration & demo
// ---------------------------------------------------------------------------

const config: DemoConfigItem[] = [
    { key: 'q', selector: state => state.query, action: actions.setQuery, defaultValue: '', history: 'replace' },
    { key: 'view', selector: state => state.view, action: actions.setView, defaultValue: DEFAULT_VIEW },
    {
        key: 'amount',
        selector: state => state.amount,
        action: actions.setAmount,
        type: 'number',
        defaultValue: DEFAULT_AMOUNT,
    },
    { key: 'tags', selector: state => state.tags, action: actions.setTags, type: 'array', defaultValue: [] },
    {
        key: 'archived',
        selector: state => state.showArchived,
        action: actions.setShowArchived,
        type: 'boolean',
        defaultValue: false,
    },
];

export const UrlStateDemo = () => {
    const [urlState, dispatch] = useReducer(reducer, initialState);

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

    // Example of how to get the values from the redux store (in this demo it's just React)
    const reduxValues = useMemo(
        () =>
            config.reduce<Record<string, unknown>>((acc, item) => {
                acc[item.key] = item.selector(urlState);
                return acc;
            }, {}),
        [urlState]
    );

    // The actual hook for managing the URL state and the callbacks to react on URL changes
    useUrlState({
        config,
        searchString: location.search,
        storeValues: reduxValues,
        navigationType,
        onUpdateStore: action => dispatch(action as DemoAction),
        onNavigate: (search, mode) => navigate({ search }, { replace: mode === 'replace' }),
    });

    const urlValue = `${BASE_PATH}/${location.search || ''}`;

    return (
        <div className='padding-15 max-width-800'>
            <LabeledElement label='URL' htmlFor='url-state-url' className='margin-bottom-15'>
                <input id='url-state-url' className='form-control' value={urlValue} readOnly />
            </LabeledElement>

            <div className='display-grid grid-cols-1 grid-cols-2-md gap-25'>
                <Card>
                    <LabeledElement label='Search query' htmlFor='url-state-query' className='margin-bottom-15'>
                        <ClearableInput
                            id='url-state-query'
                            value={urlState.query}
                            onChange={(newValue: string) => dispatch(actions.setQuery(newValue))}
                            onClear={() => dispatch(actions.setQuery(''))}
                            placeholder='Enter search value'
                        />
                    </LabeledElement>

                    <LabeledElement label='View mode' htmlFor='url-state-view' className='margin-bottom-15'>
                        <select
                            id='url-state-view'
                            className='form-control'
                            value={urlState.view}
                            onChange={event => dispatch(actions.setView(event.target.value as ViewType))}
                        >
                            {VIEW_OPTIONS.map(option => (
                                <option key={option} value={option}>
                                    {option}
                                </option>
                            ))}
                        </select>
                    </LabeledElement>

                    <LabeledElement label='Amount (default: 1)' htmlFor='url-state-amount' className='margin-bottom-15'>
                        <input
                            id='url-state-amount'
                            className='form-control'
                            type='number'
                            min='1'
                            value={urlState.amount}
                            onChange={event => {
                                const nextAmount = Number.parseInt(event.target.value, 10);
                                dispatch(actions.setAmount(Number.isFinite(nextAmount) ? nextAmount : DEFAULT_AMOUNT));
                            }}
                        />
                    </LabeledElement>

                    <LabeledElement label='Tags (picklist)' className='margin-bottom-15'>
                        <TagPicklist value={urlState.tags} onChange={tags => dispatch(actions.setTags(tags))} />
                    </LabeledElement>

                    <Checkbox
                        checked={urlState.showArchived}
                        onChange={event => dispatch(actions.setShowArchived(event.target.checked))}
                    >
                        Show archived <br />
                        (default: "false" - will be omitted from the URL)
                    </Checkbox>
                </Card>
                <Card className='margin-bottom-0'>
                    <LabeledElement label={`Current URL state (stored in ${BASE_PATH})`}>
                        <pre className='text-size-12'>{JSON.stringify(urlState, null, 2)}</pre>
                    </LabeledElement>
                </Card>
            </div>
        </div>
    );
};

// ---------------------------------------------------------------------------
// Demo components
// ---------------------------------------------------------------------------

const TAG_OPTIONS = [
    { value: 'banana', label: 'Banana' },
    { value: 'peach', label: 'Peach' },
    { value: 'pear', label: 'Pear' },
    { value: 'strawberry', label: 'Strawberry' },
] as const;

type TagPicklistProps = {
    value: string[];
    onChange: (nextValue: string[]) => void;
};

const getPicklistCheckboxClasses = (isSelected: boolean) =>
    `padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white ${
        isSelected
            ? 'border-color-highlight bg-highlight text-color-white'
            : 'hover-bg-highlight-decent text-color-darker'
    }`;

const TagPicklist = ({ value, onChange }: TagPicklistProps) => {
    const handleSelect = (selectedValue: string) => {
        const selected = new Set(value);
        if (selected.has(selectedValue)) {
            selected.delete(selectedValue);
        } else {
            selected.add(selectedValue);
        }

        const nextTags = TAG_OPTIONS.filter(option => selected.has(option.value)).map(option => option.value);
        onChange(nextTags);
    };

    return (
        <div className='display-flex gap-5 flex-wrap'>
            {TAG_OPTIONS.map(option => (
                <Checkbox
                    key={option.value}
                    onChange={() => handleSelect(option.value)}
                    checked={value.includes(option.value)}
                    custom
                    className={getPicklistCheckboxClasses(value.includes(option.value))}
                >
                    <div className='checkbox-text-wrapper'>{option.label}</div>
                </Checkbox>
            ))}
        </div>
    );
};

export default UrlStateDemo;
```

#### HTML (html)

```html
<div class="padding-15 max-width-800">
    <div class="LabeledElement display-flex flex-column margin-bottom-15">
        <label for="url-state-url" class="display-inline-block">
            <span>URL</span>
        </label>
        <input id="url-state-url" class="form-control" readonly="" value="my-service/#/">
    </div>
    <div class="display-grid grid-cols-1 grid-cols-2-md gap-25">
        <div class="bg-white rounded border shadow-default padding-15">
            <div class="LabeledElement display-flex flex-column margin-bottom-15">
                <label for="url-state-query" class="display-inline-block">
                    <span>Search query</span>
                </label>
                <div class="ClearableInput input-group">
                    <input id="url-state-query" placeholder="Enter search value" class="form-control" type="text" tabindex="0" value="">
                    <span class="clearButton hide">
                        <span class="clearButtonIcon rioglyph rioglyph-remove-sign">
                        </span>
                    </span>
                </div>
            </div>
            <div class="LabeledElement display-flex flex-column margin-bottom-15">
                <label for="url-state-view" class="display-inline-block">
                    <span>View mode</span>
                </label>
                <select id="url-state-view" class="form-control">
                    <option value="list">list</option>
                    <option value="grid">grid</option>
                </select>
            </div>
            <div class="LabeledElement display-flex flex-column margin-bottom-15">
                <label for="url-state-amount" class="display-inline-block">
                    <span>Amount (default: 1)</span>
                </label>
                <input id="url-state-amount" class="form-control" type="number" min="1" value="1">
            </div>
            <div class="LabeledElement display-flex flex-column margin-bottom-15">
                <div class="text-color-dark max-width-100pct text-size-12 display-inline-block margin-bottom-5">Tags (picklist)</div>
                <div class="display-flex gap-5 flex-wrap">
                    <label class="checkbox padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker" tabindex="0">
                        <input type="checkbox" class="padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker">
                        <div class="checkbox-text-wrapper">Banana</div>
                    </label>
                    <label class="checkbox padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker" tabindex="0">
                        <input type="checkbox" class="padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker">
                        <div class="checkbox-text-wrapper">Peach</div>
                    </label>
                    <label class="checkbox padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker" tabindex="0">
                        <input type="checkbox" class="padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker">
                        <div class="checkbox-text-wrapper">Pear</div>
                    </label>
                    <label class="checkbox padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker" tabindex="0">
                        <input type="checkbox" class="padding-x-15 padding-y-5 user-select-none rounded shadow-accent bg-white hover-bg-highlight-decent text-color-darker">
                        <div class="checkbox-text-wrapper">Strawberry</div>
                    </label>
                </div>
            </div>
            <label class="checkbox" tabindex="0">
                <input type="checkbox" class="">
                <span class="checkbox-text">
                    <span>Show archived <br>(default: "false" - will be omitted from the URL)</span>
                </span>
            </label>
        </div>
        <div class="bg-white rounded border shadow-default padding-15 margin-bottom-0">
            <div class="LabeledElement display-flex flex-column">
                <div class="text-color-dark max-width-100pct text-size-12 display-inline-block margin-bottom-5">Current URL state (stored in my-service/#)</div>
                <pre class="text-size-12">{
  "query": "",
  "view": "list",
  "amount": 1,
  "tags": [],
  "showArchived": false
}</pre>
            </div>
        </div>
    </div>
</div>
```

#### Props: useUrlState

### useUrlState

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| config | readonly UrlConfigItem<TState, any>[] | — | URL sync configuration. Each entry maps one query param key to a store selector and update action. |
| searchString | QueryString | — | Current URL search string (for example location.search). Expected shape: '?q=test&sort=-name' or ''. |
| storeValues | Record<string, unknown> | — | Current store values used by the configured selectors. This could be values from a redux store. Keys should match the config keys, values are compared against URL/defaults. |
| localStorageKey | string | — | Optional localStorage key for sticky URL state restore on initial load when managed URL params are missing. |
| navigationType | 'POP' \| 'PUSH' \| 'REPLACE' \| string | — | Router navigation type (typically from React Router useNavigationType). POP indicates browser back/forward navigation. |
| onUpdateStore | (action: any) => void | — | Store update callback invoked when URL -> store sync detects a change. Usually wraps dispatch(...). |
| onNavigate | (search: string, historyMode: 'push' \| 'replace') => void | — | Navigation callback used for store -> URL sync writes. historyMode controls whether to push or replace the history entry. |