fix: Zustand selector anti-pattern causing infinite re-render + plugin loader path

- Fix usePluginStore(s => s.getAll*()) selectors that returned new arrays
  on every call, causing infinite re-render loops and permanent spinner
- Affected: Sidebar, Settings, ContactDetail, ContactEditModal, PluginRouteRenderer
- Use usePluginStore(s => s.manifests) + useMemo instead
- Fix PluginRouteRenderer: show spinner instead of null when manifests loading
- Fix PluginLoader: @/ path replacement ../ -> ../../ for correct dynamic imports
This commit is contained in:
Agent Zero
2026-07-24 19:29:48 +02:00
parent 6e930b814e
commit 4b9cb5a098
6 changed files with 57 additions and 16 deletions
@@ -40,8 +40,9 @@ function getLazyComponent(componentPath: string): React.LazyExoticComponent<Reac
return componentCache.get(componentPath)!;
}
// Convert @/pages/Mail to ../pages/Mail for dynamic import
const importPath = componentPath.replace(/^@\//, '../');
// Convert @/pages/Mail to ../../pages/Mail for dynamic import
// PluginLoader.tsx is in src/components/plugins/, so ../../ resolves to src/
const importPath = componentPath.replace(/^@\//, '../../');
const LazyComp = lazy(() =>
import(/* @vite-ignore */ importPath).then((m) => ({
@@ -1,4 +1,6 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { usePluginStore } from '@/store/pluginStore';
import { PluginPage } from './PluginLoader';
@@ -15,10 +17,15 @@ import { PluginPage } from './PluginLoader';
*/
export function PluginRouteRenderer() {
const location = useLocation();
const getAllPageRoutes = usePluginStore((s) => s.getAllPageRoutes);
const manifests = usePluginStore((s) => s.manifests);
const loaded = usePluginStore((s) => s.loaded);
const routes = getAllPageRoutes();
const routes = useMemo(
() => manifests
.flatMap((m) => m.page_routes)
.sort((a, b) => a.order - b.order),
[manifests]
);
// Find the first matching route (exact match or prefix match for nested routes)
const matchedRoute = routes.find((r) => {
@@ -31,7 +38,6 @@ export function PluginRouteRenderer() {
if (matchedRoute) {
// Find the plugin name for display
const manifests = usePluginStore.getState().manifests;
const plugin = manifests.find((m) =>
m.page_routes.some((pr) => pr.path === matchedRoute.path)
);
@@ -45,9 +51,13 @@ export function PluginRouteRenderer() {
);
}
// If manifests haven't loaded yet, show nothing (the static router handles it)
// If manifests haven't loaded yet, show a spinner (not null/blank)
if (!loaded) {
return null;
return (
<div className="flex items-center justify-center min-h-[50vh]" role="status" aria-label="Loading">
<Loader2 className="animate-spin h-8 w-8 text-primary-500" aria-hidden="true" />
</div>
);
}
// No plugin route matched — show a simple not-found