@@ -126,13 +126,19 @@ function useField<
126126
127127 return {
128128 active : false ,
129- blur : ( ) => { } ,
130- change : ( ) => { } ,
129+ blur : ( ) => {
130+ form . blur ( name as keyof FormValues ) ;
131+ } ,
132+ change : ( value ) => {
133+ form . change ( name as keyof FormValues , value ) ;
134+ } ,
131135 data : data || { } ,
132136 dirty : false ,
133137 dirtySinceLastSubmit : false ,
134138 error : undefined ,
135- focus : ( ) => { } ,
139+ focus : ( ) => {
140+ form . focus ( name as keyof FormValues ) ;
141+ } ,
136142 initial : initialStateValue ,
137143 invalid : false ,
138144 length : undefined ,
@@ -184,6 +190,69 @@ function useField<
184190 // eslint-disable-next-line react-hooks/exhaustive-deps
185191 } , [ name , data , defaultValue , initialValue ] ) ;
186192
193+ // FIX #988: When initialValue prop changes, update the form's initialValues
194+ // for this field. This ensures that when a parent component updates initialValues
195+ // after a save operation, the field becomes pristine if the value matches.
196+ const prevInitialValueRef = React . useRef ( initialValue ) ;
197+ React . useEffect ( ( ) => {
198+ // Only run when initialValue actually changes (not on mount)
199+ if (
200+ prevInitialValueRef . current !== initialValue &&
201+ initialValue !== undefined
202+ ) {
203+ prevInitialValueRef . current = initialValue ;
204+
205+ // Get current form state
206+ const formState = form . getState ( ) ;
207+ const currentFormInitial = formState . initialValues
208+ ? getIn ( formState . initialValues , name )
209+ : undefined ;
210+
211+ // Only update if the new initialValue differs from current form initial
212+ if ( initialValue !== currentFormInitial ) {
213+ const currentValue = getIn ( formState . values , name ) ;
214+
215+ // If the current value matches the new initial value, update the form's
216+ // initialValues to reflect this. This is needed for radio buttons where
217+ // the user changes the value, then the parent saves and passes back the
218+ // new initial value that matches what the user selected.
219+ //
220+ // We need to manually update formState.initialValues and notify listeners.
221+ // Final Form doesn't expose a public API for this, so we use internal state.
222+ const fieldState = form . getFieldState ( name as keyof FormValues ) ;
223+ if ( fieldState ) {
224+ // Force an update through the field subscriber by triggering a change
225+ // to the same value, which will recalculate dirty state with new initial
226+ if ( currentValue === initialValue ) {
227+ // The value matches the new initial, so field should become pristine.
228+ // Re-register with new initialValue to update formState.initialValues.
229+ // Final Form's registerField will update initialValues when:
230+ // - value === old initial (meaning pristine before)
231+ // We need to handle the case where value === new initial but value !== old initial
232+ //
233+ // Workaround: We need to update formState.initialValues directly.
234+ // The only public API is form.setConfig('initialValues', ...) but that
235+ // resets ALL values. Instead, we use a workaround:
236+ // Trigger a re-registration which will update initialValues for this field.
237+ form . pauseValidation ( ) ;
238+ try {
239+ // Manually update initialValues via registerField with silent: false
240+ // to force notification
241+ form . registerField (
242+ name as keyof FormValues ,
243+ ( ) => { } ,
244+ { } ,
245+ { initialValue }
246+ ) ;
247+ } finally {
248+ form . resumeValidation ( ) ;
249+ }
250+ }
251+ }
252+ }
253+ }
254+ } , [ initialValue , name , form ] ) ;
255+
187256 const meta : any = { } ;
188257 addLazyFieldMetaState ( meta , state ) ;
189258 const getInputValue = ( ) => {
@@ -245,7 +314,7 @@ function useField<
245314 const input : FieldInputProps < FieldValue , T > = {
246315 name,
247316 onBlur : useConstantCallback ( ( _event ?: React . FocusEvent < any > ) => {
248- state . blur ( ) ;
317+ form . blur ( name as keyof FormValues ) ;
249318 if ( formatOnBlur ) {
250319 /**
251320 * Here we must fetch the value directly from Final Form because we cannot
@@ -254,9 +323,9 @@ function useField<
254323 * before calling `onBlur()`, but before the field has had a chance to receive
255324 * the value update from Final Form.
256325 */
257- const fieldState = form . getFieldState ( state . name as keyof FormValues ) ;
326+ const fieldState = form . getFieldState ( name as keyof FormValues ) ;
258327 if ( fieldState ) {
259- state . change ( format ( fieldState . value , state . name ) ) ;
328+ form . change ( name as keyof FormValues , format ( fieldState . value , name ) ) ;
260329 }
261330 }
262331 } ) ,
@@ -282,14 +351,16 @@ function useField<
282351 }
283352 }
284353
354+ const currentValue =
355+ form . getFieldState ( name as keyof FormValues ) ?. value ?? state . value ;
285356 const value : any =
286357 event && event . target
287- ? getValue ( event , state . value , _value , isReactNative )
358+ ? getValue ( event , currentValue , _value , isReactNative )
288359 : event ;
289- state . change ( parse ( value , name ) ) ;
360+ form . change ( name as keyof FormValues , parse ( value , name ) ) ;
290361 } ) ,
291362 onFocus : useConstantCallback ( ( _event ?: React . FocusEvent < any > ) =>
292- state . focus ( ) ,
363+ form . focus ( name as keyof FormValues ) ,
293364 ) ,
294365 get value ( ) {
295366 return getInputValue ( ) ;
0 commit comments