You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/06/11 04:58:46 UTC

(grails-forge-ui) 01/04: Restore Zustand launch state loading

This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch fix/pr-111-restore-launch-ui
in repository https://gitbox.apache.org/repos/asf/grails-forge-ui.git

commit c9d3f184120ff0cbd92ab0ab1ae356263ff3f4e9
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Thu Jun 11 00:40:21 2026 -0400

    Restore Zustand launch state loading
    
    Initialize the Zustand store before children render, reload feature data when feature-query fields change, and ignore stale async loader responses.
    
    Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
    
    Assisted-by: opencode:gpt-5.5
    
    Co-authored-by: Sisyphus <cl...@sisyphuslabs.ai>
---
 .../__tests__/useAvailableFeatures.test.jsx        |  14 ++-
 app/launch/src/state/ApplicationState.jsx          |  14 +--
 app/launch/src/state/store.js                      | 100 ++++++++++++++++-----
 3 files changed, 100 insertions(+), 28 deletions(-)

diff --git a/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx b/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx
index 1dfc913..ff268fe 100644
--- a/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx
+++ b/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx
@@ -19,6 +19,13 @@ const MockSdk = {
     }
     return types
   },
+  defaultIncludedFeatures: async ({ type }) => {
+    const types = MockTypes[type]
+    if (!types) {
+      throw new Error('Invalid Type')
+    }
+    return { features: [] }
+  },
 }
 
 beforeEach(() => {
@@ -80,11 +87,16 @@ TEST_DATA.forEach(({ initialData, hasError }) => {
       await new Promise((r) => setTimeout(r, 50))
     })
 
-    expect(container).toBeDefined()
     if (hasError) {
       expect(error).not.toBeNull()
+      expect(container.querySelector('.reloading').textContent).toEqual('[]')
     } else {
       expect(error).toBeNull()
+      expect(JSON.parse(container.querySelector('.reloading').textContent)).toEqual([
+        'a',
+        'b',
+        'c',
+      ])
     }
   })
 })
diff --git a/app/launch/src/state/ApplicationState.jsx b/app/launch/src/state/ApplicationState.jsx
index 249ba90..d6eefe6 100644
--- a/app/launch/src/state/ApplicationState.jsx
+++ b/app/launch/src/state/ApplicationState.jsx
@@ -1,22 +1,24 @@
-import { useEffect } from 'react'
-import { useAppStore } from './store'
+import { useRef } from 'react'
 import { initializeStateFactory } from './factories/initializeState'
+import { useAppStore } from './store'
 
 export default function ApplicationState({
-  initialData,
+  initialData = {},
   stateInitializer,
   children,
 }) {
-  useEffect(() => {
+  const initialized = useRef(false)
+
+  if (!initialized.current) {
+    initialized.current = true
     const initializer = typeof stateInitializer === 'function'
       ? stateInitializer
       : initializeStateFactory(initialData)
 
-    // Apply initial state to the store
     if (typeof initializer === 'function') {
       initializer(useAppStore)
     }
-  }, []) // eslint-disable-line react-hooks/exhaustive-deps
+  }
 
   return <>{children}</>
 }
diff --git a/app/launch/src/state/store.js b/app/launch/src/state/store.js
index 2aa98a9..99647f9 100644
--- a/app/launch/src/state/store.js
+++ b/app/launch/src/state/store.js
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from 'react'
+import { useCallback, useEffect, useMemo } from 'react'
 import { create } from 'zustand'
 
 import { sharableLink } from '../helpers/Routing'
@@ -11,6 +11,17 @@ const INITIAL_FORM_DATA_STORAGE_KEY = 'INITIAL_FORM_DATA'
 
 const versionLoader = loadVersions()
 
+const optionsRequestKey = (state) => state.getBaseUrl()
+
+const featuresRequestKey = (state) => JSON.stringify({
+  baseUrl: state.getBaseUrl(),
+  appType: state.appType,
+  javaVersion: state.javaVersion,
+  servlet: state.servlet,
+  gorm: state.gorm,
+  reloading: state.reloading,
+})
+
 // Main application store
 export const useAppStore = create((set, get) => ({
   // Initial values
@@ -38,13 +49,16 @@ export const useAppStore = create((set, get) => ({
   // Options (loaded from API)
   options: {},
   optionsLoading: false,
+  optionsRequestId: 0,
 
   // Features
   availableFeatures: [],
   availableFeaturesLoading: false,
   availableFeaturesError: null,
+  availableFeaturesRequestId: 0,
   defaultIncludedFeatures: [],
   defaultIncludedFeaturesLoading: false,
+  defaultIncludedFeaturesRequestId: 0,
 
   // Actions
   setInitialValues: (values) => set({ initialValues: values }),
@@ -143,12 +157,18 @@ export const useAppStore = create((set, get) => ({
   loadOptions: async () => {
     const sdk = get().getSdk()
     if (!sdk) return
-    set({ optionsLoading: true })
+    const requestId = get().optionsRequestId + 1
+    const requestKey = optionsRequestKey(get())
+    set({ optionsLoading: true, optionsRequestId: requestId })
     try {
       const options = await sdk.selectOptions()
-      set({ options, optionsLoading: false })
-    } catch (_e) {
-      set({ optionsLoading: false })
+      if (get().optionsRequestId === requestId && optionsRequestKey(get()) === requestKey) {
+        set({ options, optionsLoading: false })
+      }
+    } catch {
+      if (get().optionsRequestId === requestId && optionsRequestKey(get()) === requestKey) {
+        set({ optionsLoading: false })
+      }
     }
   },
   loadAvailableFeatures: async () => {
@@ -159,41 +179,66 @@ export const useAppStore = create((set, get) => ({
       set({ availableFeatures: [], availableFeaturesLoading: false })
       return
     }
-    set({ availableFeaturesLoading: true, availableFeaturesError: null })
+    const requestId = get().availableFeaturesRequestId + 1
+    const requestKey = featuresRequestKey(get())
+    set({
+      availableFeaturesLoading: true,
+      availableFeaturesError: null,
+      availableFeaturesRequestId: requestId,
+    })
     try {
       const { features } = await sdk.features({ type: appType, form })
-      set({ availableFeatures: features, availableFeaturesLoading: false })
+      if (get().availableFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) {
+        set({ availableFeatures: features, availableFeaturesLoading: false })
+      }
     } catch (error) {
-      set({ availableFeatures: [], availableFeaturesLoading: false, availableFeaturesError: error })
+      if (get().availableFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) {
+        set({ availableFeatures: [], availableFeaturesLoading: false, availableFeaturesError: error })
+      }
     }
   },
   loadDefaultIncludedFeatures: async () => {
     const sdk = get().getSdk()
     const appType = get().appType
     const form = get().getStarterForm()
-    if (!sdk || !form.type) {
+    if (!sdk || !appType) {
       set({ defaultIncludedFeatures: [] })
       return
     }
-    set({ defaultIncludedFeaturesLoading: true })
+    const requestId = get().defaultIncludedFeaturesRequestId + 1
+    const requestKey = featuresRequestKey(get())
+    set({
+      defaultIncludedFeaturesLoading: true,
+      defaultIncludedFeaturesRequestId: requestId,
+    })
     try {
       const { features } = await sdk.defaultIncludedFeatures({ type: appType, form })
-      set({ defaultIncludedFeatures: features, defaultIncludedFeaturesLoading: false })
-    } catch (_e) {
-      set({ defaultIncludedFeatures: [], defaultIncludedFeaturesLoading: false })
+      if (get().defaultIncludedFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) {
+        set({ defaultIncludedFeatures: features, defaultIncludedFeaturesLoading: false })
+      }
+    } catch {
+      if (get().defaultIncludedFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) {
+        set({ defaultIncludedFeatures: [], defaultIncludedFeaturesLoading: false })
+      }
     }
   },
   resetForm: async () => {
-    const options = get().options
+    let options = get().options
+    if (!options.type) {
+      await get().loadOptions()
+      options = get().options
+    }
+    if (!options.type) return
+
     const resets = formResets({})
     set({
       name: resets.name,
       package: resets.package,
-      appType: options.type?.defaultOption?.value,
-      servlet: options.servlet?.defaultOption?.value,
-      gorm: options.gorm?.defaultOption?.value,
-      reloading: options.reloading?.defaultOption?.value,
-      javaVersion: options.jdkVersion?.defaultOption?.value,
+      appType: options.type.defaultOption.value,
+      servlet: options.servlet.defaultOption.value,
+      gorm: options.gorm.defaultOption.value,
+      reloading: options.reloading.defaultOption.value,
+      javaVersion: options.jdkVersion.defaultOption.value,
       features: {},
     })
   },
@@ -481,7 +526,7 @@ export const useGetStarterForm = () => {
 }
 
 export const useResetStarterForm = () => {
-  return () => useAppStore.getState().resetForm()
+  return useCallback(() => useAppStore.getState().resetForm(), [])
 }
 
 // Load options when version changes
@@ -499,6 +544,10 @@ export function useLoadOptionsEffect() {
 export function useLoadFeaturesEffect() {
   const appType = useAppStore((s) => s.appType)
   const selectedVersion = useAppStore((s) => s.selectedVersion)
+  const javaVersion = useAppStore((s) => s.javaVersion)
+  const servlet = useAppStore((s) => s.servlet)
+  const gorm = useAppStore((s) => s.gorm)
+  const reloading = useAppStore((s) => s.reloading)
   const loadAvailableFeatures = useAppStore((s) => s.loadAvailableFeatures)
   const loadDefaultIncludedFeatures = useAppStore((s) => s.loadDefaultIncludedFeatures)
   useEffect(() => {
@@ -506,5 +555,14 @@ export function useLoadFeaturesEffect() {
       loadAvailableFeatures()
       loadDefaultIncludedFeatures()
     }
-  }, [appType, selectedVersion, loadAvailableFeatures, loadDefaultIncludedFeatures])
+  }, [
+    appType,
+    selectedVersion,
+    javaVersion,
+    servlet,
+    gorm,
+    reloading,
+    loadAvailableFeatures,
+    loadDefaultIncludedFeatures,
+  ])
 }