Skip to main content

Overview

Logo Soup’s core engine is framework-agnostic. All framework adapters are thin wrappers (~30-80 lines) that bridge the engine’s subscribe / getSnapshot interface into a framework’s reactivity model. If your framework isn’t supported yet, you can easily build your own adapter.

Core Engine Interface

Every adapter works with the same core engine:
import { createLogoSoup } from "@sanity-labs/logo-soup";

const engine = createLogoSoup();

// Subscribe to state changes
const unsubscribe = engine.subscribe(() => {
  const state = engine.getSnapshot();
  // Update your framework's reactive state
});

// Trigger processing
engine.process({ logos: ["/logo1.svg"], baseSize: 48 });

// Get current state synchronously
const { status, normalizedLogos, error } = engine.getSnapshot();

// Clean up
engine.destroy();

Engine API

type LogoSoupEngine = {
  /** Trigger processing. Call when inputs change. */
  process(options: ProcessOptions): void;

  /** Subscribe to state changes. Returns unsubscribe function. */
  subscribe(listener: () => void): () => void;

  /** Get current immutable snapshot. Same reference if nothing changed. */
  getSnapshot(): LogoSoupState;

  /** Cleanup blob URLs, cancel in-flight work */
  destroy(): void;
};
type LogoSoupState = {
  status: "idle" | "loading" | "ready" | "error";
  normalizedLogos: NormalizedLogo[];
  error: Error | null;
};

Adapter Pattern

All adapters follow this pattern:
  1. Create the engine (once per component instance)
  2. Subscribe to state changes
  3. Update reactive state when engine emits
  4. Watch reactive inputs and call engine.process() when they change
  5. Cleanup on component unmount

Examples by Framework

React uses useSyncExternalStore for external subscriptions:
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { createLogoSoup } from "@sanity-labs/logo-soup";

export function useLogoSoup(options: ProcessOptions) {
  const engineRef = useRef<ReturnType<typeof createLogoSoup> | null>(null);
  if (!engineRef.current) {
    engineRef.current = createLogoSoup();
  }
  const engine = engineRef.current;

  // Must be referentially stable
  const subscribe = useCallback(
    (onStoreChange: () => void) => engine.subscribe(onStoreChange),
    [engine],
  );
  const getSnapshot = useCallback(() => engine.getSnapshot(), [engine]);

  const state = useSyncExternalStore(subscribe, getSnapshot);

  // Trigger processing when options change
  useEffect(() => {
    engine.process(options);
  }, [engine, options.logos, options.baseSize /* ... */]);

  // Cleanup on unmount
  useEffect(() => () => engine.destroy(), [engine]);

  return {
    isLoading: state.status === "loading",
    isReady: state.status === "ready",
    normalizedLogos: state.normalizedLogos,
    error: state.error,
  };
}

Key Considerations

1. State Synchronization

The engine emits on every state change. Your adapter should update the framework’s reactive state:
engine.subscribe(() => {
  frameworkState.value = engine.getSnapshot();
});

2. Reactive Processing

Watch for input changes and re-trigger processing:
// React
useEffect(() => {
  engine.process(options);
}, [options.logos, options.baseSize]);

// Vue
watchEffect(() => {
  engine.process({ logos: toValue(options.logos) });
});

// Solid
createEffect(() => {
  engine.process(optionsFn());
});

3. Cleanup

Always clean up on unmount:
// Unsubscribe
const unsubscribe = engine.subscribe(...);
onCleanup(unsubscribe);

// Destroy engine
onCleanup(() => engine.destroy());

4. Referential Stability

The snapshot reference only changes when values actually change. This is important for performance:
const prev = engine.getSnapshot();
const next = engine.getSnapshot();

// If nothing changed:
prev === next; // true

// After a change:
prev !== next; // true

Testing Your Adapter

Test these scenarios:
  1. Initial load — Process logos on mount
  2. State updates — Verify reactive state changes
  3. Input changes — Re-process when options change
  4. Error handling — Handle failed image loads
  5. Cleanup — No memory leaks on unmount
  6. Concurrent updates — Cancel previous processing when inputs change

TypeScript

Import types from the core package:
import type {
  LogoSoupEngine,
  LogoSoupState,
  ProcessOptions,
  LogoSource,
  NormalizedLogo,
  AlignmentMode,
  BackgroundColor,
} from "@sanity-labs/logo-soup";

See Also

Build docs developers (and LLMs) love