> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Persisting app state in URLs

> Use syncInternalState to store your Flows custom app's view state in the URL, so users can share links that open the app exactly where they left off. This guide is for developers building Flows custom apps who want shareable deep links.

Flows custom apps run inside an iframe and can't manage the browser URL directly. Call `syncInternalState` to store your app's view state in the `customAppInternalState` URL search param. This makes the URL shareable. Anyone who opens it sees the same view. Cognite Data Fusion (CDF) passes the stored value back as `initialState` the next time the app mounts.

## Prerequisites

Before you start, ensure you have:

* A **Flows** custom app created and running locally. See [Getting started with Flows custom apps](/cdf/flows/guides/getting-started).
* **`@cognite/app-sdk`** installed.

## How it works

When your app calls `syncInternalState`, CDF serializes your state object into the `customAppInternalState` URL search param. The state can capture anything that defines where the user was. That includes not just which page is open, but also component-level detail: which panels are collapsed, what values are prefilled in a form, which rows are expanded in a table, what filters are active. For example:

```json theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
{
  "activeTab": "trends",
  "selectedAssetId": "7518432",
  "sidebarCollapsed": true,
  "filters": { "status": "active", "minValue": 42 },
  "prefilledForm": { "tag": "21PT1234", "unit": "bar" }
}
```

CDF URL-encodes that and appends it to the browser URL:

```http wrap theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
https://your-cluster.fusion.cognite.com/my-project/flows/apps/my-app
  ?customAppInternalState=%7B%22activeTab%22%3A%22trends%22%2C%22selectedAssetId%22%3A...%7D
```

The user can copy that URL and share it. When someone else (or the same user after navigating away) opens the link, CDF decodes the param and passes it back to your app as `initialState`. The app opens with the same tab, filters, and form values already in place.

## Implement state syncing

<Steps>
  <Step title="Read initialState on mount">
    Destructure `initialState` from `connectToHostApp`. If it exists, parse it and restore your app's state before the first render.

    ```tsx wrap theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    import { useEffect, useState } from "react";
    import { connectToHostApp, type HostAppAPI } from "@cognite/app-sdk";

    function App() {
      const [api, setApi] = useState<HostAppAPI | null>(null);
      const [activeTab, setActiveTab] = useState("overview");

      useEffect(() => {
        connectToHostApp()
          .then(({ api, initialState }) => {
            setApi(api);
            if (initialState) {
              try {
                setActiveTab(JSON.parse(initialState).activeTab);
              } catch {
                // ignore malformed state
              }
            }
          })
          .catch(() => {
            // Not running inside CDF — continue with defaults
          });
      }, []);

      // ...
    }
    ```

    Always wrap `JSON.parse` in a `try`/`catch`. Saved state may be malformed if the schema changed between deploys.

    <Check>
      Open your app with a `customAppInternalState` value in the URL (or reload after syncing state in step 2). The restored tab, filters, and other fields match the saved state instead of your defaults.
    </Check>
  </Step>

  <Step title="Call syncInternalState when state changes">
    Call `syncInternalState` after any user action that changes meaningful state. Pass a JSON-serialized string.

    ```tsx wrap theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    function handleTabChange(tab: string) {
      setActiveTab(tab);
      void api?.syncInternalState(JSON.stringify({ activeTab: tab }));
    }
    ```

    Pass the complete state object on every call. `syncInternalState` replaces the stored value; it does not merge.

    <Check>
      Change a persisted value (for example, switch tabs). The browser URL updates with a new `customAppInternalState` search param that reflects your change.
    </Check>
  </Step>
</Steps>

## Complete example

Because `syncInternalState` **replaces** the stored value on every call, the safest approach is to keep all persisted fields in a single state object and update them together. That way there is one place to serialize from and one place to restore to. No field silently overwrites another.

```tsx wrap theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
import { useEffect, useRef, useState } from "react";
import { connectToHostApp, type HostAppAPI } from "@cognite/app-sdk";

interface PersistedState {
  activeTab: string;
  sidebarCollapsed: boolean;
  filters: { status: string };
}

const DEFAULT_STATE: PersistedState = {
  activeTab: "overview",
  sidebarCollapsed: false,
  filters: { status: "all" },
};

export function App() {
  const [api, setApi] = useState<HostAppAPI | null>(null);
  const [state, setState] = useState<PersistedState>(DEFAULT_STATE);
  const apiRef = useRef<HostAppAPI | null>(null);

  useEffect(() => {
    connectToHostApp()
      .then(({ api, initialState }) => {
        apiRef.current = api;
        setApi(api);
        if (initialState) {
          try {
            setState({ ...DEFAULT_STATE, ...JSON.parse(initialState) });
          } catch {
            // malformed — keep defaults
          }
        }
      })
      .catch(() => {
        // standalone dev — continue without host
      });
  }, []);

  function updateState(patch: Partial<PersistedState>) {
    setState((prev) => {
      const next = { ...prev, ...patch };
      void apiRef.current?.syncInternalState(JSON.stringify(next));
      return next;
    });
  }

  return (
    <>
      <Sidebar
        collapsed={state.sidebarCollapsed}
        onToggle={(collapsed) => updateState({ sidebarCollapsed: collapsed })}
      />
      <TabBar
        activeTab={state.activeTab}
        onChange={(tab) => updateState({ activeTab: tab })}
      />
      <FilterBar
        filters={state.filters}
        onChange={(filters) => updateState({ filters })}
      />
    </>
  );
}
```

`updateState` merges the patch into the current state and immediately syncs the full object. No field is ever lost.

## What to include in persisted state

Include state that defines the user's current view and that they would want to return to:

| Include                          | Exclude                                           |
| -------------------------------- | ------------------------------------------------- |
| Active tab or page               | Loading flags (`isLoading`, `isFetching`)         |
| Selected item IDs                | Server-fetched data (re-fetch on mount)           |
| Applied filters and sort order   | Truly ephemeral UI state (open dropdown, tooltip) |
| Search queries                   | Error messages                                    |
| Collapsed / expanded panel state | Sensitive values (tokens, passwords)              |
| Prefilled form values            | Large blobs or binary data                        |
| Zoom level or scroll position    | N/A                                               |

Keep persisted state small. Store IDs and view parameters, not full data objects. Fetch data from the API on mount using the restored IDs.

<Warning>
  Browsers impose a maximum URL length (typically around 2,000 characters, though it varies by browser and server). Because the state is URL-encoded, large state objects can exceed this limit and cause the URL to be truncated or rejected. Avoid storing large state objects. If your serialized state grows beyond a few hundred characters, store only the minimal keys needed to reconstruct the view.
</Warning>

## Further reading

* [Auth API](/cdf/flows/reference/api/auth) — `connectToHostApp`, `HostAppAPI`, and helpers.
* [Integrating AI agents with Flows](/cdf/flows/guides/ai_agent_integration) — Share app state with the built-in agent.
* [Run your app locally](/cdf/flows/guides/running-locally) — Dev server, HTTPS, and CDF iframe.
