Labaratory is a fully featured React component that provides a modern GraphQL playground experience (collections, tabs, history, preflight scripts, environment variables, tests, etc.). It ships with an opinionated UI and a context provider that exposes granular hooks for integrating with your own storage or analytics layers.
This document explains how to embed the component, what data it needs, and how to react to user changes through the available callbacks.
import { Labaratory } from "@/components/labaratory/labaratory";
export const Playground = () => {
return (
<Labaratory
defaultEndpoint="https://api.spacex.land/graphql/"
onEndpointChange={(endpoint) => {
localStorage.setItem("lab-endpoint", endpoint ?? "");
}}
defaultOperations={[]}
onOperationCreate={(operation) => console.log("created", operation)}
onOperationUpdate={(operation) => console.log("updated", operation)}
onOperationDelete={(operation) => console.log("deleted", operation)}
/>
);
};The component renders the full UI and injects a LabaratoryProvider, so any nested component can call useLabaratory() to access the current state.
Labaratory is controlled via two complementary mechanisms:
- Default Values –
default*props let you hydrate the playground from persisted data (e.g., localStorage, database). These are only read during initialization. - Event Callbacks –
on*props fire whenever users create/update/delete entities within the playground. Use them to keep external storage in sync or to trigger side effects (analytics, notifications, etc.).
If you provide both a default value and a callback for the same entity, you can make the playground fully persistent without touching its internals.
defaultEndpoint?: string | nullonEndpointChange?: (endpoint: string | null) => void
defaultCollections?: LabaratoryCollection[]onCollectionsChange?: (collections: LabaratoryCollection[]) => voidonCollectionCreate?: (collection: LabaratoryCollection) => voidonCollectionUpdate?: (collection: LabaratoryCollection) => voidonCollectionDelete?: (collection: LabaratoryCollection) => voidonCollectionOperationCreate?: (collection: LabaratoryCollection, operation: LabaratoryCollectionOperation) => voidonCollectionOperationUpdate?: (collection: LabaratoryCollection, operation: LabaratoryCollectionOperation) => voidonCollectionOperationDelete?: (collection: LabaratoryCollection, operation: LabaratoryCollectionOperation) => void
defaultOperations?: LabaratoryOperation[]defaultActiveOperationId?: stringonOperationsChange?: (operations: LabaratoryOperation[]) => voidonActiveOperationIdChange?: (operationId: string) => voidonOperationCreate?: (operation: LabaratoryOperation) => voidonOperationUpdate?: (operation: LabaratoryOperation) => voidonOperationDelete?: (operation: LabaratoryOperation) => void
defaultHistory?: LabaratoryHistory[]onHistoryChange?: (history: LabaratoryHistory[]) => voidonHistoryCreate?: (history: LabaratoryHistory) => voidonHistoryUpdate?: (history: LabaratoryHistory) => voidonHistoryDelete?: (history: LabaratoryHistory) => void
defaultTabs?: LabaratoryTab[]defaultActiveTabId?: string | nullonTabsChange?: (tabs: LabaratoryTab[]) => voidonActiveTabIdChange?: (tabId: string | null) => void
defaultPreflight?: LabaratoryPreflight | nullonPreflightChange?: (preflight: LabaratoryPreflight | null) => void
defaultEnv?: LabaratoryEnv | nullonEnvChange?: (env: LabaratoryEnv | null) => void
defaultSettings?: LabaratorySettings | nullonSettingsChange?: (settings: LabaratorySettings | null) => void
defaultTests?: LabaratoryTest[]onTestsChange?: (tests: LabaratoryTest[]) => void
useLabaratory() also exposes openAddCollectionDialog, openUpdateEndpointDialog, and openAddTestDialog so that external buttons can toggle the built-in dialogs.
Inside any descendant of Labaratory, call the hook to access live state and actions:
import { useLabaratory } from "@/components/labaratory/context";
const RunButton = () => {
const { runActiveOperation, endpoint } = useLabaratory();
return (
<button
disabled={!endpoint}
onClick={() => runActiveOperation(endpoint!, { env: { variables: {} } })}
>
Run
</button>
);
};All actions returned by the hook (collections, operations, history, tabs, preflight, env, settings, tests) stay in sync with the UI.
The snippet below demonstrates how to persist operations and history to localStorage using the granular callbacks:
const STORAGE_KEY = "labaratory-data";
const loadData = () => {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
} catch {
return {};
}
};
const save = (partial: any) => {
const current = loadData();
localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...current, ...partial }));
};
const data = loadData();
export function PersistentPlayground() {
return (
<Labaratory
defaultOperations={data.operations ?? []}
onOperationsChange={(operations) => save({ operations })}
onOperationCreate={(operation) =>
console.info("operation created", operation)
}
defaultHistory={data.history ?? []}
onHistoryDelete={(history) => console.info("history deleted", history)}
onHistoryChange={(history) => save({ history })}
/>
);
}