fix(picker): late overlay registration re-renders the picker — no more reload races
The @pro overlay import is dynamic, so board/component registration can land AFTER the picker mounted and memoized its lists. allBoards had frozen deps — if the picker rendered first, overlay boards (M5Stack Core, Cardputer, Pimoroni, C6) vanished for the whole session while their ONLINE ads were already hidden; the component grid + ONLINE component ads flipped between ad cards and real entries depending on who won the reload race (different SVG, ONLINE badge appearing and disappearing, wrong hover thumbnail). proBoardRegistry and ComponentRegistry.mergeComponents now bump a version and notify subscribers; the picker's allBoards / filteredComponents / visibleComponentAds memos key off those via useSyncExternalStore — same contract as proRoutes and registerProExamples.
This commit is contained in:
parent
75680a408c
commit
5beccf94a6
|
|
@ -9,7 +9,7 @@
|
|||
* - Click to select and add component
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ComponentRegistry } from '../services/ComponentRegistry';
|
||||
|
|
@ -33,7 +33,12 @@ interface CardHoverApi {
|
|||
import type { BoardKind } from '../types/board';
|
||||
import { BOARD_KIND_LABELS } from '../types/board';
|
||||
import { isProBoardKind } from '../lib/proBoardGate';
|
||||
import { getProBoard, listProBoards } from '../lib/proBoardRegistry';
|
||||
import {
|
||||
getProBoard,
|
||||
listProBoards,
|
||||
subscribeProBoards,
|
||||
getProBoardsVersion,
|
||||
} from '../lib/proBoardRegistry';
|
||||
import {
|
||||
ONLINE_ONLY_BOARD_ADS,
|
||||
ONLINE_ONLY_COMPONENT_ADS,
|
||||
|
|
@ -204,12 +209,30 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
}
|
||||
|
||||
return components;
|
||||
}, [searchQuery, selectedCategory, registry, isLoading]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery, selectedCategory, registry, isLoading, registryVersion]);
|
||||
|
||||
// Late-overlay registrations must re-render an already-mounted picker:
|
||||
// the @pro import is dynamic, so boards/components can register AFTER the
|
||||
// first render. Without these subscriptions the memos below freeze on the
|
||||
// pre-registration state (boards missing, ONLINE ads instead of the real
|
||||
// components - and which one you got depended on a reload race).
|
||||
const proBoardsVersion = useSyncExternalStore(
|
||||
subscribeProBoards,
|
||||
getProBoardsVersion,
|
||||
getProBoardsVersion,
|
||||
);
|
||||
const registryVersion = useSyncExternalStore(
|
||||
registry.subscribe,
|
||||
registry.getVersion,
|
||||
registry.getVersion,
|
||||
);
|
||||
|
||||
// Boards list: static OSS kinds + overlay-registered boards (proBoardRegistry).
|
||||
const allBoards = useMemo(() => {
|
||||
return [...ALL_BOARDS, ...(listProBoards().map((d) => d.kind) as BoardKind[])];
|
||||
}, []);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [proBoardsVersion]);
|
||||
|
||||
// Online-only component ads: shown where the real component would sit, and
|
||||
// hidden automatically in any build whose registry has the real component
|
||||
|
|
@ -223,7 +246,8 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
(selectedCategory === 'all' || ad.category === selectedCategory) &&
|
||||
(!q || ad.label.toLowerCase().includes(q)),
|
||||
);
|
||||
}, [registry, isLoading, searchQuery, selectedCategory]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [registry, isLoading, searchQuery, selectedCategory, registryVersion, proBoardsVersion]);
|
||||
|
||||
// Get available categories
|
||||
const categories = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,21 @@ export interface ProBoardDef {
|
|||
|
||||
const registry = new Map<string, ProBoardDef>();
|
||||
|
||||
// Registration happens when the overlay's dynamic import lands - potentially
|
||||
// AFTER a consumer rendered and memoized its board list. Subscribers re-render
|
||||
// on every registration (same contract as proRoutes / registerProExamples).
|
||||
let version = 0;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
export function subscribeProBoards(cb: () => void): () => void {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
}
|
||||
|
||||
export function getProBoardsVersion(): number {
|
||||
return version;
|
||||
}
|
||||
|
||||
export function registerProBoards(defs: ProBoardDef[]): void {
|
||||
for (const def of defs) {
|
||||
registry.set(def.kind, def);
|
||||
|
|
@ -106,6 +121,8 @@ export function registerProBoards(defs: ProBoardDef[]): void {
|
|||
(BOARD_KIND_FQBN as Record<string, string | null>)[kind] = def.fqbn;
|
||||
if (def.supportsMicroPython) BOARD_SUPPORTS_MICROPYTHON.add(kind);
|
||||
}
|
||||
version++;
|
||||
for (const l of listeners) l();
|
||||
}
|
||||
|
||||
export function getProBoard(kind: string): ProBoardDef | undefined {
|
||||
|
|
|
|||
|
|
@ -273,8 +273,24 @@ export class ComponentRegistry {
|
|||
byId.set(extra.id, extra);
|
||||
}
|
||||
this.processMetadata(Array.from(byId.values()));
|
||||
// The overlay merges AFTER its dynamic import lands - notify subscribers so
|
||||
// an already-open picker recomputes (ONLINE ad cards vs real components).
|
||||
this._version++;
|
||||
for (const l of this._changeListeners) l();
|
||||
}
|
||||
|
||||
private _version = 0;
|
||||
private _changeListeners = new Set<() => void>();
|
||||
|
||||
/** Monotonic counter bumped on every mergeComponents() - pair with subscribe
|
||||
* in useSyncExternalStore so late overlay merges re-render consumers. */
|
||||
getVersion = (): number => this._version;
|
||||
|
||||
subscribe = (cb: () => void): (() => void) => {
|
||||
this._changeListeners.add(cb);
|
||||
return () => this._changeListeners.delete(cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get components by category
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue