React Integration

Open in Claude

Patterns for using the Vim Connect App SDK in React applications

The Vim Connect App SDK works with any JavaScript framework, but since most Vim apps are built with React, here are proven patterns for managing SDK state, subscriptions, and lifecycle.

SDK Provider Pattern

Wrap your app with a provider that handles initialization and makes the SDK available to all components:

// src/providers/vim-sdk-provider.tsx
'use client';

import { createContext, useContext, useEffect, useRef, useState } from 'react';
import { initVimSDK, type VimSDK } from '@vimconnect/app-sdk';

interface VimSDKContextValue {
  sdk: VimSDK | null;
  status: 'loading' | 'connected' | 'error';
  error: string | null;
}

const VimSDKContext = createContext<VimSDKContextValue>({
  sdk: null,
  status: 'loading',
  error: null,
});

export function useVimSDK() {
  return useContext(VimSDKContext);
}

interface VimSDKProviderProps {
  accessToken: string;
  children: React.ReactNode;
}

export function VimSDKProvider({ accessToken, children }: VimSDKProviderProps) {
  const [state, setState] = useState<VimSDKContextValue>({
    sdk: null,
    status: 'loading',
    error: null,
  });
  const initRef = useRef(false);

  useEffect(() => {
    if (initRef.current) return;
    initRef.current = true;

    initVimSDK({ accessToken, debug: true })
      .then((sdk) => {
        sdk.hub.setActivationStatus('ENABLED');
        setState({ sdk, status: 'connected', error: null });
      })
      .catch((err) => {
        setState({ sdk: null, status: 'error', error: err.message });
      });
  }, [accessToken]);

  return (
    <VimSDKContext.Provider value={state}>
      {children}
    </VimSDKContext.Provider>
  );
}

Usage:

// src/app/app/page.tsx
function App({ accessToken }: { accessToken: string }) {
  return (
    <VimSDKProvider accessToken={accessToken}>
      <PatientPanel />
    </VimSDKProvider>
  );
}

Subscribing to Context Changes

The most common pattern is subscribing to context changes inside useEffect and syncing with React state:

// src/hooks/usePatientContext.ts
import { useEffect, useState } from 'react';
import { useVimSDK } from '@/providers/vim-sdk-provider';

export function usePatientContext() {
  const { sdk } = useVimSDK();
  const [patient, setPatient] = useState<any>(null);

  useEffect(() => {
    if (!sdk) return;

    const unsubscribe = sdk.ehr.context.onChange(
      'chart_open:patient',
      (prev, curr) => {
        setPatient(curr ?? null);
      },
    );

    return unsubscribe;
  }, [sdk]);

  return patient;
}
// src/components/patient-panel.tsx
function PatientPanel() {
  const patient = usePatientContext();

  if (!patient) return <div>No patient selected</div>;

  const demo = patient.fields?.demographics;
  return (
    <div>
      <h2>{demo?.firstName} {demo?.lastName}</h2>
      <p>DOB: {demo?.dateOfBirth}</p>
    </div>
  );
}

Subscribing to Workflow Events

import { useEffect, useState } from 'react';
import { useVimSDK } from '@/providers/vim-sdk-provider';

export function useWorkflowEvents() {
  const { sdk } = useVimSDK();
  const [events, setEvents] = useState<any[]>([]);

  useEffect(() => {
    if (!sdk) return;

    const unsubscribe = sdk.ehr.workflow.on(
      ['chart_open', 'encounter_open'],
      (event) => {
        setEvents((prev) => [
          { type: event.type, timestamp: Date.now(), data: event },
          ...prev.slice(0, 49), // keep last 50
        ]);
      },
    );

    return unsubscribe;
  }, [sdk]);

  return events;
}

Performing Writeback

Writeback operations require checking capabilities and requesting permission. Here's a reusable pattern:

import { useCallback } from 'react';
import { useVimSDK } from '@/providers/vim-sdk-provider';

export function useEncounterWriteback() {
  const { sdk } = useVimSDK();

  const updateEncounter = useCallback(
    async (
      data: Record<string, any>,
      fields: string[],
      mode: 'override' | 'append' = 'override',
    ) => {
      if (!sdk) throw new Error('SDK not initialized');

      // Check capability
      const cap = sdk.ehr.context.encounter.getCapability('update');
      if (!cap.available) {
        throw new Error(
          cap.reason === 'not_in_context'
            ? 'No active encounter'
            : 'Encounter updates not supported on this EHR',
        );
      }

      // Request permission for disruptive operations
      if (cap.disruptive && cap.permissionState === 'requestable') {
        const result = await sdk.ehr.context.encounter.requestPermission('update', { fields });
        if (result === 'denied') throw new Error('Permission denied by provider');
      }

      // Perform the update
      return sdk.ehr.context.encounter.update(data, { mode });
    },
    [sdk],
  );

  return { updateEncounter };
}

Usage in a component:

function AddDiagnosis({ icdCode, description }: { icdCode: string; description: string }) {
  const { updateEncounter } = useEncounterWriteback();
  const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');

  async function handleAdd() {
    setStatus('saving');
    try {
      await updateEncounter(
        { assessment: { diagnoses: [{ code: icdCode, description }] } },
        ['assessment.diagnoses'],
        'append',
      );
      setStatus('saved');
    } catch (err: any) {
      console.error('Writeback failed:', err.message);
      setStatus('error');
    }
  }

  return (
    <button onClick={handleAdd} disabled={status === 'saving'}>
      {status === 'saving' ? 'Adding...' : `Add ${icdCode}`}
    </button>
  );
}

Hub Controls

import { useVimSDK } from '@/providers/vim-sdk-provider';

function NotificationButton() {
  const { sdk } = useVimSDK();

  function showAlert() {
    sdk?.hub.pushNotification.show({
      title: 'Action Required',
      text: 'Review the care gap for this patient',
      notificationId: `alert-${Date.now()}`,
      type: 'warning',
      timeoutInSec: 15,
      actionButtons: {
        rightButton: {
          text: 'Review',
          buttonStyle: 'PRIMARY',
          openAppButton: true,
          callback: () => console.log('Provider clicked Review'),
        },
      },
    });
  }

  return <button onClick={showAlert}>Show Alert</button>;
}

Tips

  • Always clean up subscriptions. Return the unsubscribe function from useEffect to prevent memory leaks.
  • Use refs for initialization. Prevent double-initialization in React StrictMode with a useRef(false) guard.
  • Keep the SDK instance stable. Initialize once in a provider, access everywhere via context. Don't call initVimSDK() in multiple components.
  • Handle the loading state. The SDK takes a moment to connect — show a loading state until status === 'connected'.
  • Wrap useSearchParams in Suspense. Next.js App Router requires a Suspense boundary around components that use useSearchParams().

On this page