Skip to content

useUserState ​

useUserState reads, writes, and drafts user state for the project mounted by SkyStateProvider.

Authentication Setup ​

User state requires an authenticated end user. Put user-state UI behind an auth gate:

tsx
import { useStatus } from '@skystate/react';

export function UserStateAuthGate({ children }: { children: React.ReactNode }) {
  const { auth } = useStatus();

  if (auth.status !== 'authenticated') {
    return <button onClick={auth.loginWithRedirect}>Sign in</button>;
  }

  return <>{children}</>;
}

See useStatus for the full auth API.


Basic Examples ​

Read and update a value ​

tsx
import { useUserState } from '@skystate/react';

export function ThemeToggle() {
  const { value: theme, set: setTheme } = useUserState('theme', 'dark');

  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      Theme: {theme}
    </button>
  );
}

Draft a form before saving ​

tsx
import { useUserState } from '@skystate/react';

export function ProfileEditor() {
  const { draft: profile } = useUserState('profile', { displayName: '' });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        profile.save();
      }}
    >
      <input
        value={profile.displayValue?.displayName ?? ''}
        onChange={(event) => {
          profile.set((current) => ({
            ...current,
            displayName: event.target.value,
          }));
        }}
      />
      <button type="submit" disabled={!profile.isPending}>Save</button>
      <button type="button" onClick={profile.discard}>Discard</button>
    </form>
  );
}

Show loading and errors ​

tsx
import { useStatus } from '@skystate/react';

export function UserStateGate({ children }: { children: React.ReactNode }) {
  const { health } = useStatus();

  if (health.status === 'loading') return <Skeleton />;
  if (health.status === 'error') {
    return <div>{health.error.message}</div>;
  }

  return <>{children}</>;
}

API ​

ts
function useUserState<T = unknown>(
  key: string,
): UseUserStateResult<T | null>

function useUserState<T>(
  key: string,
  fallback: T,
): UseUserStateResult<T | null>

type UseUserStateResult<V> = {
  value: V;
  set: (value: V | ((prev: V) => V)) => void;
  clear: () => void;
  syncStatus: 'unset' | 'syncing' | 'synced';
  draft: UseUserStateDraft<V>;
};

Both overloads return UseUserStateResult<T | null>: a fallback does not narrow value (see Fallback semantics). set(undefined) is a type error on every overload; removal goes through clear().

Parameters ​

ParameterDescription
keyTop-level user-state key such as theme, profile, or preferences.
fallbackReturned while user state is not ready or when the key is absent. value stays T | null even with a fallback; see Fallback semantics.

Keys are top-level names; see key rules.

Returns ​

CallReturn
useUserState<T>(key){ value: T | null; set; clear; syncStatus; draft }.
useUserState<T>(key, fallback){ value: T | null; set; clear; syncStatus; draft }.

set(value) ​

ts
set(value: T | null | ((prev: T | null) => T | null)): void

Writes a new value for the selected key. The visible value updates immediately. A rejected write reverts only its own optimistic value; other pending writes stay applied. The rejection is reported through onError; see useStatus().health for which rejections also surface there. See Error Handling for the full per-status contract.

Every write that reaches the API counts toward usage; see client.userState.set() for which writes are dropped before sending.

Functional updaters must be pure. The updater runs once at call time for the immediate visible update, then again at save time against the latest server-confirmed value, and it is re-run when the SDK refetches and replays the write after a version conflict (HTTP 412). Side effects in the updater repeat on every run.

clear() ​

ts
clear(): void

Removes the key from user state. clear() is the only removal path: set(undefined) is a type error. Clearing a key that is already absent is a benign no-op.

syncStatus ​

Reports the sync state of the key's stored value, independent of any staged draft:

ValueMeaning
'unset'The server has no value for this key; the stored value is the fallback (or null).
'syncing'A set() for this key is in flight; the stored value is the unconfirmed local write.
'synced'The stored value equals the last server-confirmed value.

Draft Handle ​

ts
type UseUserStateDraft<T> = {
  displayValue: T;
  isPending: boolean;
  set: (value: T | ((displayValue: T) => T)) => void;
  save: () => void;
  discard: () => void;
};
MemberDescription
displayValueDrafted value when present, otherwise the committed value. Safe for controlled inputs.
isPendingWhether a local draft exists.
set(value)Updates the local draft only.
save()Saves the current draft value.
discard()Drops the local draft value.

Cross-Tab Synchronization ​

Committed writes synchronize across browser tabs automatically: when one tab's write is confirmed by the server, other same-origin tabs mounted on the same account, project, and environment refetch and pick up the new value. Only signed-in tabs react. See Cross-Tab Sync for where this is unavailable.


Public Contract ​

ContractDetail
Auth requiredUser state reads and writes require a signed-in end user.
Top-level keysKeys are top-level names, not nested paths.
Local updateset updates the visible value immediately.
StatusUse useStatus().health for loading and error state.
ErrorsLoad and save failures surface through onError; see useStatus().health for which failures also flip health; credential rejections end the session (see auth).
MetadataUse the console, CLI, or REST API for inspection, audit, or history metadata.