Skip to content

useStatus: Auth and Health ​

The React SDK separates identity and health from state data:

  • useStatus() returns { auth, health }: auth status with auth actions, plus overall client health.
  • Keyed usePublicState(key) returns { value }.
  • Keyed useUserState(key) returns { value, set, clear, syncStatus, draft }, where draft exposes displayValue, isPending, set, save, and discard.

All hooks must be called inside a component that is a descendant of SkyStateProvider.


useStatus ​

typescript
function useStatus(): { auth: SkyStateAuth; health: SkyStateHealth }

type SkyStateAuth = (
  | {
      status: 'unauthenticated';
      detail: { reason: 'signed_out' } | { reason: 'expired'; error: SkyStateAuthError };
    }
  | { status: 'authenticating' }
  | {
      status: 'authenticated';
      idToken: string;
      claims: Readonly<Record<string, unknown>>;
      name: string | null;
      email: string | null;
      sessionPersisted: boolean;
    }
) & {
  loginWithRedirect: () => Promise<void>;
  logout: () => Promise<void>;
};
tsx
import { useStatus } from '@skystate/react';

export function LoginButton() {
  const { auth } = useStatus();
  const isAuthenticated = auth.status === 'authenticated';

  return (
    <button onClick={isAuthenticated ? auth.logout : auth.loginWithRedirect}>
      {isAuthenticated ? 'Log out' : 'Log in'}
    </button>
  );
}

Narrow on the authenticated branch before reading idToken, claims, name, or email. name and email are always present on the authenticated snapshot and typed string | null: they are null when the ID token has no usable claim, never omitted.

On the unauthenticated branch, detail.reason says why: 'signed_out' for a plain signed-out session, or 'expired' (with the rejecting error) when the session ended because the credential was rejected.

logout() clears the local session immediately, then fires a best-effort server call to revoke the refresh token; it never waits for or fails on the revocation. The lower-level core clearAuthTokens() API is local-only token deletion; React apps should prefer useStatus().auth.logout() for user sign-out.


Health ​

typescript
type SkyStateHealth =
  | { status: 'loading' }
  | { status: 'ok' }
  | { status: 'error'; error: SkyStateHealthError | AsyncMisuseError };
tsx
import { useStatus } from '@skystate/react';

export function StatusBar() {
  const { health } = useStatus();

  if (health.status === 'loading') return <div>Loading...</div>;

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

  return <div>Connected</div>;
}

health summarizes the whole client: loading while the client is initializing or public/user state is still loading, error while an operational error (network, server, rate limit, quota, configuration, protocol) is active, otherwise ok. Retries keep running in the background, but health does not clear on a retry alone: it returns to ok on the next successful write or state refetch. One case never self-heals: an invalid_path misuse error from a broken functional updater pins health to error for the client's lifetime (until remount). A write rejected with unsupported_media_type (415) is dropped rather than retried, so health stays error until a later write succeeds or state is refetched; see error codes.


Auth Errors and onError ​

The SkyStateProvider accepts an optional onError callback for provider, state, and auth-pipeline errors. When onError is omitted, only failures classified into health or auth remain observable to your code. Per-write patch_* content rejections are not in either set: without onError the only visible effect is the rolled-back optimistic value, so pass onError if your app needs to report failed saves.

tsx
<SkyStateProvider
  account="acc_example"
  project="my-app"
  environment="production"
  onError={(err) => {
    console.error(err.code, err.message);
  }}
>
  <App />
</SkyStateProvider>

Token refresh failures ​

Temporary sign-in renewal failures keep the current session active while the SDK retries in the background.

Expired or invalid sessions transition to unauthenticated with detail: { reason: 'expired', error }, so the user can sign in again; onError receives the same error.

Session persistence ​

If the token storage adapter cannot persist tokens during login, the in-memory session continues but will not survive a page reload. This is not reported through onError or health; check the authenticated snapshot instead:

tsx
const { auth } = useStatus();

if (auth.status === 'authenticated' && !auth.sessionPersisted) {
  showToast('Session will not persist after reload - storage unavailable.');
}

Permission failures (403) ​

A 401 or 403 on user-state load or save either ends the session (auth becomes unauthenticated with detail: { reason: 'expired', error } and onError fires with code: 'authentication') or, when the rejected credential was already superseded, retries silently; see the write-result contract for the rules. Check that the provider is mounted with the right account, project, and environment, that the project's auth-provider settings allow the signed-in user, and that the credential is a SkyState end-user token issued by the project's end-user sign-in flow, not a developer token from sky login: user-state routes require the end-user role, so a developer token is authenticated but rejected with 403.


User State with Auth ​

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

export function Preferences() {
  const { auth, health } = useStatus();
  const { value: theme, set: setTheme } = useUserState('theme', 'dark');

  if (auth.status === 'authenticating') return null;

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

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

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