ChemPal Documentation - v1.12.0
    Preparing search index...

    Variable MoleculeSpinnerConst

    Renders the molecule a search query refers to as a slowly rotating ball-and-stick model, as a drop-in replacement for a generic loading spinner.

    The structure comes from PubChem: the query is resolved to a CID, and its 3D conformer fetched — falling back to the flat 2D record for the ionic and inorganic compounds PubChem computes no conformer for, which a planar wobble keeps legible. When nothing resolves, fallback is rendered instead, so the caller always has something on screen.

    three.js is loaded through a dynamic import, so the WebGL renderer stays out of the initial bundle and only downloads once a structure is ready to draw. The component is memoized and holds its scene in a ref: the surrounding loading UI re-renders constantly as results arrive, and rebuilding the scene on each of those would restart the animation and leak WebGL contexts.

    The spinner props (see MoleculeSpinnerProps).

    The rotating structure, or the supplied fallback.

    <MoleculeSpinner
    query={executedQuery}
    fallback={<IconSpinner><img src="/static/images/cubane-loader.png" /></IconSpinner>}
    />
    export const MoleculeSpinner = memo(function MoleculeSpinner(props: MoleculeSpinnerProps) {
    const {
    query,
    size = DEFAULT_SIZE,
    spinSeconds = DEFAULT_SPIN_SECONDS,
    atomScale,
    showHydrogens = true,
    pending,
    fallback,
    onStatusChange,
    } = props;

    const [molecule, setMolecule] = useState<Molecule | undefined>();
    const [unavailable, setUnavailable] = useState(false);
    const [sceneFailed, setSceneFailed] = useState(false);
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const handleRef = useRef<MoleculeSceneHandle | undefined>(undefined);

    // Read through refs inside effects that must not re-run when these change.
    const statusRef = useRef(onStatusChange);
    statusRef.current = onStatusChange;
    const sizeRef = useRef(size);
    sizeRef.current = size;
    const spinRef = useRef(spinSeconds);
    spinRef.current = spinSeconds;

    useEffect(() => {
    let cancelled = false;
    setMolecule(undefined);
    setUnavailable(false);
    setSceneFailed(false);

    if (query.trim() === '') {
    statusRef.current?.({ state: 'idle' });
    return;
    }

    statusRef.current?.({ state: 'loading' });
    const startedAt = performance.now();

    const resolve = async () => {
    const resolved = await resolveMolecule(query);
    if (cancelled) return;

    if (!resolved) {
    setUnavailable(true);
    statusRef.current?.({ state: 'unavailable', elapsedMs: performance.now() - startedAt });
    return;
    }

    const drawn = showHydrogens ? resolved.molecule : stripHydrogens(resolved.molecule);
    setMolecule(drawn);
    statusRef.current?.({
    state: 'ready',
    cid: resolved.cid,
    recordType: resolved.recordType,
    atomCount: drawn.atoms.length,
    bondCount: drawn.bonds.length,
    elapsedMs: performance.now() - startedAt,
    });
    };

    void resolve();
    return () => {
    cancelled = true;
    };
    }, [query, showHydrogens]);

    useEffect(() => {
    const canvas = canvasRef.current;
    if (!molecule || !canvas) return;

    let cancelled = false;
    const build = async () => {
    try {
    const { createMoleculeScene } = await import('@/utils/molecule/scene');
    if (cancelled) return;
    handleRef.current = createMoleculeScene(canvas, molecule, {
    size: sizeRef.current,
    spinSeconds: spinRef.current,
    atomScale,
    reducedMotion: prefersReducedMotion(),
    });
    } catch (error) {
    // WebGL can be unavailable entirely (blocklisted GPU, too many live contexts,
    // a non-rendering test environment). Drop back to the caller's fallback rather
    // than leaving an empty canvas or an unhandled rejection.
    console.error('Could not render molecule; falling back:', error);
    if (cancelled) return;
    setSceneFailed(true);
    statusRef.current?.({ state: 'unavailable' });
    }
    };

    void build();
    return () => {
    cancelled = true;
    handleRef.current?.dispose();
    handleRef.current = undefined;
    };
    }, [molecule, atomScale]);

    useEffect(() => {
    handleRef.current?.setSize(size);
    }, [size]);

    useEffect(() => {
    handleRef.current?.setSpinSeconds(spinSeconds);
    }, [spinSeconds]);

    // Derived synchronously rather than from the status callback, so the very first render
    // with a query already shows `pending` — there is no frame where `fallback` flashes.
    if (molecule && !sceneFailed) {
    return (
    <canvas
    ref={canvasRef}
    data-testid="molecule-spinner"
    style={{ width: size, height: size, display: 'block' }}
    />
    );
    }
    if (query.trim() === '' || unavailable || sceneFailed) return <>{fallback}</>;
    return <>{pending}</>;
    });