Refactor components for improved state management #24
Conversation
…nd optimize state management for drag and wall loading logic
…agement and improve performance with useMemo and useCallback
…tore.ts. replacing any across all files
…ove manufacturer handling
Fix linter errors
refactor(EditorApp, AddHoldModal): improve hold instance handling and…
…irect meta tags in components
…ving distinctId and improving type safety in various components
…ing configuration
There was a problem hiding this comment.
Code Review
This pull request performs a significant refactoring of the frontend editor, introducing comprehensive TypeScript interfaces to replace 'any' types and optimizing component performance through hooks like useMemo and useCallback. It also updates several dependencies, removes react-helmet-async, and streamlines PostHog analytics integration. Key feedback includes a critical warning that removing '/gym' from the Vite proxy targets will break development API requests. Additionally, the reviewer noted an invalid configuration property in the PostHog initialization, a potential O(N^2) performance bottleneck in the recursive hold tree rendering, and the lack of error handling for optimistic updates during hold deletions.
| const targets = ['/api', '/admin', '/static', '/media']; | ||
| return Object.fromEntries(targets.map((p) => [p, backendUrl])); |
There was a problem hiding this comment.
The removal of /gym from the proxy targets will break API requests in the development environment. Several core components (e.g., AddHoldModal, SidebarHoldsSection, FileManager) rely on endpoints starting with /gym/ to fetch stock data, update layouts, and manage session collections.
| const targets = ['/api', '/admin', '/static', '/media']; | |
| return Object.fromEntries(targets.map((p) => [p, backendUrl])); | |
| const targets = ['/api', '/gym', '/admin', '/static', '/media']; |
| posthog.init(apiKey, { | ||
| api_host: host, | ||
| ui_host: 'https://eu.posthog.com', | ||
| defaults: '2026-01-30', |
There was a problem hiding this comment.
defaults is not a valid configuration option for posthog.init in the PostHog JS SDK. Additionally, the value '2026-01-30' appears to be a future date, which is likely a mistake or intended for a different property. Please verify the intended configuration (e.g., bootstrap, persistence, or a custom property).
| setDeletedHoldTypeIds((prev) => new Set([...prev, String(hold.hold_type?.id)])); | ||
| setLocallyAddedHolds((prev) => prev.filter((m) => m.hold_type?.id !== hold.hold_type?.id)); | ||
| posthog.capture('hold removed from collection', { hold_name: hold.name, hold_id: hold.id, session_id: session_data?.id }); | ||
| await authenticatedFetch( | ||
| `${API_URL}/gym/changeholdtosessioncollection/${session_data.id}/0/${hold.id}/` | ||
| ); |
There was a problem hiding this comment.
The handleDelete function performs an optimistic update by removing the hold from local state before the API call completes. If the authenticatedFetch fails, the UI will remain in an inconsistent state without the user knowing the deletion failed. Consider adding a try-catch block to roll back the local state changes if the network request fails.
| function renderHoldTree(parentId: string | null) { | ||
| return objects | ||
| .filter((o) => o.type === "hold" && o.parentId === parentId && o.url) // Only render objects with valid URLs | ||
| .filter((o) => o.type === "hold" && o.parentId === parentId && o.url) |
There was a problem hiding this comment.
Filtering the entire objects array inside the recursive renderHoldTree function results in O(N^2) complexity. As the number of placed holds grows, this will cause significant performance degradation during renders. It is recommended to pre-group objects by parentId (e.g., using a Map) before starting the recursive render process.
There was a problem hiding this comment.
Pull request overview
This PR refactors parts of the frontend to improve state handling and typing in the editor, updates analytics instrumentation, and removes react-helmet-async usage across the app.
Changes:
- Removed
react-helmet-asyncand replaced Helmet-based metadata with inline<title>/<meta>JSX in showcase pages. - Improved editor typing by centralizing shared types in
store.tsand reducinganyusage across editor utilities/components. - Adjusted editor UI state handling (memoization, local hold add/delete tracking) and updated PostHog event capture calls.
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates the “project purpose” video link. |
| frontend/vite.config.ts | Changes dev proxy targets (removes /gym). |
| frontend/src/shared/auth/AuthProvider.tsx | Adds eslint suppression for react-refresh export rule. |
| frontend/src/shared/analytics/posthog.ts | Updates PostHog initialization options. |
| frontend/src/main.tsx | Removes HelmetProvider wrapper. |
| frontend/src/features/showcase/HomePage.tsx | Removes Helmet usage; adds inline title/meta/JSON-LD and updates PostHog capture call. |
| frontend/src/features/showcase/PartnersPage.tsx | Removes Helmet usage; adds inline title/meta. |
| frontend/src/features/showcase/ContactPage.tsx | Removes Helmet usage; adds inline title/meta. |
| frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx | Replaces any with local types and tightens parsing/handling. |
| frontend/src/features/editor/utils/HandleLoadSession.tsx | Introduces SessionData typing and tighter object typing/casts. |
| frontend/src/features/editor/utils/HandleAddHold.tsx | Uses shared store types for hold models and callback typing. |
| frontend/src/features/editor/stubs/Hold360.tsx | Improves typing for hold prop and removes any access. |
| frontend/src/features/editor/store.ts | Adds shared TS interfaces/types (HoldType/HoldModel/SessionData/etc.). |
| frontend/src/features/editor/EditorApp.tsx | Memoizes hold model derivation and adjusts query settings / event capture. |
| frontend/src/features/editor/components/useDragPreview.ts | Minor typing/const cleanups and safer property checks. |
| frontend/src/features/editor/components/SidebarHoldsSection.tsx | Refactors local hold list management and PostHog capture calls; adds typing. |
| frontend/src/features/editor/components/Sidebar.tsx | Refactors hold model processing and current-holds tracking for modal. |
| frontend/src/features/editor/components/ModelViewer.tsx | Refines material typing when extracting texture maps. |
| frontend/src/features/editor/components/MainCanvas.tsx | Refactors wall-loading overlay logic and drag state derivations. |
| frontend/src/features/editor/components/HoldInspector.tsx | Refines event listener typing. |
| frontend/src/features/editor/components/FileManager.tsx | Tightens typing and updates PostHog calls for save flows. |
| frontend/src/features/editor/components/AddHoldModal.tsx | Tightens stock typing and filtering logic. |
| frontend/package.json | Removes react-helmet-async, bumps uuid. |
| frontend/package-lock.json | Lockfile updates matching dependency changes. |
| .env.example | Updates PostHog host example value. |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Sync batch size with current wall count — React's recommended pattern for resetting | ||
| // state when derived data changes (avoids setState-in-effect anti-pattern). | ||
| if (wallBatchSize !== wallObjects.length) { | ||
| setWallBatchSize(wallObjects.length); | ||
| setWallLoadCount(0); | ||
| setForceLoaded(false); | ||
| } |
| proxy: (() => { | ||
| const backendUrl = process.env.BACKEND_URL ?? 'http://localhost:8000'; | ||
| const targets = ['/api', '/gym', '/admin', '/static', '/media']; | ||
| const targets = ['/api', '/admin', '/static', '/media']; |
| createRoot(document.getElementById('root')!).render( | ||
| <StrictMode> | ||
| <HelmetProvider> | ||
| <QueryClientProvider client={queryClient}> | ||
| <AuthProvider> | ||
| <App /> | ||
| </AuthProvider> | ||
| </QueryClientProvider> | ||
| </HelmetProvider> | ||
| <QueryClientProvider client={queryClient}> | ||
| <AuthProvider> | ||
| <App /> | ||
| </AuthProvider> | ||
| </QueryClientProvider> | ||
| </StrictMode> |
| <> | ||
| <Helmet> | ||
| <title>SetRsoft – Éditeur 3D Bouldering en ligne</title> | ||
| <meta name="description" content="Éditeur 3D virtuel pour créer vos blocs d'escalade bouldering. Simulation, routesetting, partage de séquences." /> | ||
| <script type="application/ld+json">{JSON.stringify({ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "SetRsoft", | ||
| "applicationCategory": "SportsApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Éditeur 3D de blocs d'escalade bouldering en ligne. Routesetting virtuel.", | ||
| "url": "https://setrsoft.com", | ||
| "offers": { "@type": "Offer", "price": "0" } | ||
| })}</script> | ||
| </Helmet> | ||
| <title>SetRsoft – Éditeur 3D Bouldering en ligne</title> | ||
| <meta name="description" content="Éditeur 3D virtuel pour créer vos blocs d'escalade bouldering. Simulation, routesetting, partage de séquences." /> | ||
| <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "SetRsoft", | ||
| "applicationCategory": "SportsApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Éditeur 3D de blocs d'escalade bouldering en ligne. Routesetting virtuel.", | ||
| "url": "https://setrsoft.com", | ||
| "offers": { "@type": "Offer", "price": "0" } | ||
| }) }} /> |
| <> | ||
| <Helmet> | ||
| <title>Partenaires – SetRsoft</title> | ||
| <meta name="description" content="Salles d'escalade partenaires de SetRsoft, logiciel de routesetting 3D bouldering." /> | ||
| </Helmet> | ||
| <title>Partenaires – SetRsoft</title> | ||
| <meta name="description" content="Salles d'escalade partenaires de SetRsoft, logiciel de routesetting 3D bouldering." /> | ||
| <div className="flex flex-col items-center justify-center py-24 px-4 text-center animate-fade-in"> |
| <> | ||
| <Helmet> | ||
| <title>Contact – SetRsoft</title> | ||
| <meta name="description" content="Contactez l'équipe SetRsoft pour intégrer l'éditeur 3D dans votre salle d'escalade bouldering." /> | ||
| </Helmet> | ||
| <title>Contact – SetRsoft</title> | ||
| <meta name="description" content="Contactez l'équipe SetRsoft pour intégrer l'éditeur 3D dans votre salle d'escalade bouldering." /> | ||
| <div className="flex flex-col items-center justify-center py-24 px-4 text-center animate-fade-in"> |
| @@ -58,9 +55,7 @@ const Sidebar = ({ | |||
| onClose={() => setAddHoldModalOpen(false)} | |||
| session_data={session_data} | |||
| onHoldAdded={handleHoldAddedFromModal} | |||
| currentHolds={ | |||
| holdsSectionRef.current?.getCurrentHolds() || processedHoldModels | |||
| } | |||
| currentHolds={[...processedHoldModels, ...locallyAddedHolds]} | |||
| /> | |||
| const handleDragStart = | ||
| (model: HoldModel) => (e: React.MouseEvent | React.TouchEvent) => { | ||
| e.preventDefault(); | ||
| startDrag({ | ||
| type: "hold", | ||
| url: model.hold_type.glb_url, | ||
| url: (model.hold_type.glb_url as string | undefined) ?? '', | ||
| customRotation: 0, | ||
| }); |
| @@ -58,7 +59,7 @@ export const useHandleLoadSession = (session_data: any) => { | |||
|
|
|||
| const newObjects = wallToUse ? [wallToUse, ...sessionHolds] : sessionHolds; | |||
|
|
|||
| setObjects(newObjects); | |||
| setObjects(newObjects as import("../store").PlacedObject[]); | |||
| setWallColors(data.wallColors || {}); | |||
No description provided.