Skip to content

Framework Integration

Problem

Implement framework integration in a production-friendly way with @vielzeug/fetchit while keeping setup and cleanup explicit.

Runnable Example

The snippet below is copy-paste runnable in a TypeScript project with @vielzeug/fetchit installed.

Complete examples showing how to integrate Fetchit with React, Vue, Svelte, and Web Components. All patterns use createApi for HTTP and createQuery + createMutation for data management.

Basic Integration (Inline)

tsx
import { createApi, createMutation, createQuery } from '@vielzeug/fetchit';
import { useEffect, useState } from 'react';

const api = createApi({ baseUrl: 'https://api.example.com' });
const qc = createQuery({ staleTime: 5_000 });

function UserProfile({ userId }: { userId: number }) {
  const [state, setState] = useState<QueryState<User>>(
    () =>
      qc.getState<User>(['users', userId]) ?? {
        data: undefined,
        error: null,
        status: 'idle',
        updatedAt: 0,
        isPending: false,
        isSuccess: false,
        isError: false,
        isIdle: true,
      },
  );

  useEffect(() => {
    const unsub = qc.subscribe<User>(['users', userId], setState);
    qc.query({
      key: ['users', userId],
      fn: ({ signal }) => api.get('/users/{id}', { params: { id: userId }, signal }),
    }).catch(() => {});
    return unsub;
  }, [userId]);

  if (state.isPending) return <div>Loading…</div>;
  if (state.isError) return <div>Error: {state.error!.message}</div>;
  if (!state.data) return <div>Not found</div>;
  return <div>{state.data.name}</div>;
}
vue
<script setup lang="ts">
import { createApi, createQuery } from '@vielzeug/fetchit';
import { ref, onMounted, onUnmounted } from 'vue';

const props = defineProps<{ userId: number }>();
const api = createApi({ baseUrl: 'https://api.example.com' });
const qc = createQuery({ staleTime: 5_000 });

const state = ref({ data: null as User | null, isPending: false, error: null as Error | null });
let unsub: (() => void) | undefined;

onMounted(() => {
  unsub = qc.subscribe<User>(['users', props.userId], (s) => {
    state.value = { data: s.data ?? null, isPending: s.isPending, error: s.error };
  });
  qc.query({
    key: ['users', props.userId],
    fn: ({ signal }) => api.get<User>('/users/{id}', { params: { id: props.userId }, signal }),
  }).catch(() => {});
});

onUnmounted(() => unsub?.());
</script>

<template>
  <div v-if="state.isPending">Loading…</div>
  <div v-else-if="state.error">Error: {{ state.error.message }}</div>
  <div v-else-if="state.data">{{ state.data.name }}</div>
  <div v-else>Not found</div>
</template>
svelte
<script lang="ts">
  import { createApi, createQuery } from '@vielzeug/fetchit';
  import { onDestroy } from 'svelte';

  export let userId: number;

  const api = createApi({ baseUrl: 'https://api.example.com' });
  const qc  = createQuery({ staleTime: 5_000 });

  let data: User | undefined;
  let isPending = true;
  let error: Error | null = null;

  const unsub = qc.subscribe<User>(['users', userId], (s) => {
    data = s.data; isPending = s.isPending; error = s.error;
  });

  qc.query({
    key: ['users', userId],
    fn: ({ signal }) => api.get<User>('/users/{id}', { params: { id: userId }, signal }),
  }).catch(() => {});

  onDestroy(unsub);
</script>

{#if isPending}
  <div>Loading…</div>
{:else if error}
  <div>Error: {error.message}</div>
{:else if data}
  <div>{data.name}</div>
{:else}
  <div>Not found</div>
{/if}
ts
import { createApi, createQuery } from '@vielzeug/fetchit';

const api = createApi({ baseUrl: 'https://api.example.com' });
const qc = createQuery({ staleTime: 5_000 });

class UserCard extends HTMLElement {
  #unsub?: () => void;

  connectedCallback() {
    const id = Number(this.getAttribute('user-id'));
    this.#unsub = qc.subscribe<User>(['users', id], (state) => this.#render(state));
    qc.query({ key: ['users', id], fn: ({ signal }) => api.get('/users/{id}', { params: { id }, signal }) }).catch(
      () => {},
    );
  }
  disconnectedCallback() {
    this.#unsub?.();
  }

  #render(state: QueryState<User>) {
    this.innerHTML = state.isPending
      ? '<p>Loading…</p>'
      : state.error
        ? `<p>Error: ${state.error.message}</p>`
        : state.data
          ? `<p>${state.data.name}</p>`
          : '<p>Not found</p>';
  }
}
customElements.define('user-card', UserCard);

Reusable Hook/Composable

tsx
// hooks/useQuery.ts
import { createApi, createQuery, type QueryState } from '@vielzeug/fetchit';
import { useEffect, useState } from 'react';

const api = createApi({ baseUrl: 'https://api.example.com' });
const qc = createQuery({ staleTime: 5_000 });

export function useUser(userId: number) {
  const [state, setState] = useState<QueryState<User>>(
    () =>
      qc.getState<User>(['users', userId]) ?? {
        data: undefined,
        error: null,
        status: 'idle',
        updatedAt: 0,
        isPending: false,
        isSuccess: false,
        isError: false,
        isIdle: true,
      },
  );

  useEffect(() => {
    const unsub = qc.subscribe<User>(['users', userId], setState);
    qc.query({
      key: ['users', userId],
      fn: ({ signal }) => api.get<User>('/users/{id}', { params: { id: userId }, signal }),
    }).catch(() => {});
    return unsub;
  }, [userId]);

  return state;
}

// UserProfile.tsx
function UserProfile({ userId }: { userId: number }) {
  const { data, isPending, error } = useUser(userId);
  if (isPending) return <div>Loading…</div>;
  if (error) return <div>Error: {error.message}</div>;
  return data ? <div>{data.name}</div> : <div>Not found</div>;
}
ts
// composables/useUser.ts
import { createApi, createQuery } from '@vielzeug/fetchit';
import { ref, onScopeDispose, type Ref } from 'vue';

const api = createApi({ baseUrl: 'https://api.example.com' });
const qc = createQuery({ staleTime: 5_000 });

export function useUser(userId: number) {
  const data = ref<User | null>(null);
  const isPending = ref(true);
  const error = ref<Error | null>(null);

  const unsub = qc.subscribe<User>(['users', userId], (s) => {
    data.value = s.data ?? null;
    isPending.value = s.isPending;
    error.value = s.error;
  });

  qc.query({
    key: ['users', userId],
    fn: ({ signal }) => api.get<User>('/users/{id}', { params: { id: userId }, signal }),
  }).catch(() => {});

  onScopeDispose(unsub);
  return { data, isPending, error };
}

Expected Output

  • The example runs without type errors in a standard TypeScript setup.
  • The main flow produces the behavior described in the recipe title.

Common Pitfalls

  • Forgetting cleanup/dispose calls can leak listeners or stale state.
  • Skipping explicit typing can hide integration issues until runtime.
  • Not handling error branches makes examples harder to adapt safely.