Skip to main content
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:

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:
{
  "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:
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

1

Read initialState on mount

Destructure initialState from connectToHostApp. If it exists, parse it and restore your app’s state before the first render.
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.
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.
2

Call syncInternalState when state changes

Call syncInternalState after any user action that changes meaningful state. Pass a JSON-serialized string.
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.
Change a persisted value (for example, switch tabs). The browser URL updates with a new customAppInternalState search param that reflects your change.

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.
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:
IncludeExclude
Active tab or pageLoading flags (isLoading, isFetching)
Selected item IDsServer-fetched data (re-fetch on mount)
Applied filters and sort orderTruly ephemeral UI state (open dropdown, tooltip)
Search queriesError messages
Collapsed / expanded panel stateSensitive values (tokens, passwords)
Prefilled form valuesLarge blobs or binary data
Zoom level or scroll positionN/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.
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.

Further reading

Last modified on June 24, 2026