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,
+ ])
}