11'use client'
22
3- import { useMemo , useRef } from 'react'
3+ import { useMemo , useRef , useState } from 'react'
44import { useStore } from '@tanstack/react-store'
55import { FieldApi , functionalUpdate } from '@tanstack/form-core'
66import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
77import type {
8+ AnyFieldApi ,
9+ AnyFieldMeta ,
810 DeepKeys ,
911 DeepValue ,
1012 FieldAsyncValidateOrFn ,
@@ -195,17 +197,103 @@ export function useField<
195197) {
196198 // Keep a snapshot of options so that React Compiler doesn't
197199 // wrongly optimize fieldApi.
198- const optsRef = useRef ( opts )
199- optsRef . current = opts
200+ const [ prevOptions , setPrevOptions ] = useState ( ( ) => ( {
201+ form : opts . form ,
202+ name : opts . name ,
203+ } ) )
200204
201- const fieldApi = useMemo ( ( ) => {
202- const api = new FieldApi ( {
203- ...optsRef . current ,
204- form : opts . form ,
205- name : opts . name ,
205+ const [ fieldApi , setFieldApi ] = useState ( ( ) => {
206+ return new FieldApi ( {
207+ ...opts ,
206208 } )
209+ } )
210+
211+ // We only want to
212+ // update on name changes since those are at risk of becoming stale. The field
213+ // state must be up to date for the internal JSX render.
214+ // The other options can freely be in `fieldApi.update`
215+ if ( prevOptions . form !== opts . form || prevOptions . name !== opts . name ) {
216+ setFieldApi (
217+ new FieldApi ( {
218+ ...opts ,
219+ } ) ,
220+ )
221+ setPrevOptions ( { form : opts . form , name : opts . name } )
222+ }
223+
224+ const reactiveStateValue = useStore ( fieldApi . store , ( state ) => state . value )
225+ const reactiveMetaIsTouched = useStore (
226+ fieldApi . store ,
227+ ( state ) => state . meta . isTouched ,
228+ )
229+ const reactiveMetaIsBlurred = useStore (
230+ fieldApi . store ,
231+ ( state ) => state . meta . isBlurred ,
232+ )
233+ const reactiveMetaIsDirty = useStore (
234+ fieldApi . store ,
235+ ( state ) => state . meta . isDirty ,
236+ )
237+ const reactiveMetaErrorMap = useStore (
238+ fieldApi . store ,
239+ ( state ) => state . meta . errorMap ,
240+ )
241+ const reactiveMetaErrorSourceMap = useStore (
242+ fieldApi . store ,
243+ ( state ) => state . meta . errorSourceMap ,
244+ )
245+ const reactiveMetaIsValidating = useStore (
246+ fieldApi . store ,
247+ ( state ) => state . meta . isValidating ,
248+ )
249+
250+ // This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler.
251+ const extendedFieldApi = useMemo ( ( ) => {
252+ const reactiveFieldApi = {
253+ ...fieldApi ,
254+ get state ( ) {
255+ return {
256+ value : reactiveStateValue ,
257+ get meta ( ) {
258+ return {
259+ ...fieldApi . state . meta ,
260+ isTouched : reactiveMetaIsTouched ,
261+ isBlurred : reactiveMetaIsBlurred ,
262+ isDirty : reactiveMetaIsDirty ,
263+ errorMap : reactiveMetaErrorMap ,
264+ errorSourceMap : reactiveMetaErrorSourceMap ,
265+ isValidating : reactiveMetaIsValidating ,
266+ } satisfies AnyFieldMeta
267+ } ,
268+ } satisfies AnyFieldApi [ 'state' ]
269+ } ,
270+ }
207271
208- const extendedApi : typeof api &
272+ const extendedApi : FieldApi <
273+ TParentData ,
274+ TName ,
275+ TData ,
276+ TOnMount ,
277+ TOnChange ,
278+ TOnChangeAsync ,
279+ TOnBlur ,
280+ TOnBlurAsync ,
281+ TOnSubmit ,
282+ TOnSubmitAsync ,
283+ TOnDynamic ,
284+ TOnDynamicAsync ,
285+ TFormOnMount ,
286+ TFormOnChange ,
287+ TFormOnChangeAsync ,
288+ TFormOnBlur ,
289+ TFormOnBlurAsync ,
290+ TFormOnSubmit ,
291+ TFormOnSubmitAsync ,
292+ TFormOnDynamic ,
293+ TFormOnDynamicAsync ,
294+ TFormOnServer ,
295+ TPatentSubmitMeta
296+ > &
209297 ReactFieldApi <
210298 TParentData ,
211299 TFormOnMount ,
@@ -219,16 +307,21 @@ export function useField<
219307 TFormOnDynamicAsync ,
220308 TFormOnServer ,
221309 TPatentSubmitMeta
222- > = api as never
310+ > = reactiveFieldApi as never
223311
224312 extendedApi . Field = Field as never
225313
226314 return extendedApi
227- // We only want to
228- // update on name changes since those are at risk of becoming stale. The field
229- // state must be up to date for the internal JSX render.
230- // The other options can freely be in `fieldApi.update`
231- } , [ opts . form , opts . name ] )
315+ } , [
316+ fieldApi ,
317+ reactiveStateValue ,
318+ reactiveMetaIsTouched ,
319+ reactiveMetaIsBlurred ,
320+ reactiveMetaIsDirty ,
321+ reactiveMetaErrorMap ,
322+ reactiveMetaErrorSourceMap ,
323+ reactiveMetaIsValidating ,
324+ ] )
232325
233326 useIsomorphicLayoutEffect ( fieldApi . mount , [ fieldApi ] )
234327
@@ -252,7 +345,7 @@ export function useField<
252345 : undefined ,
253346 )
254347
255- return fieldApi
348+ return extendedFieldApi
256349}
257350
258351/**
@@ -655,13 +748,7 @@ export const Field = (<
655748
656749 const jsxToDisplay = useMemo (
657750 ( ) => functionalUpdate ( children , fieldApi as any ) ,
658- /**
659- * The reason this exists is to fix an issue with the React Compiler.
660- * Namely, functionalUpdate is memoized where it checks for `fieldApi`, which is a static type.
661- * This means that when `state.value` changes, it does not trigger a re-render. The useMemo explicitly fixes this problem
662- */
663- // eslint-disable-next-line react-hooks/exhaustive-deps
664- [ children , fieldApi , fieldApi . state . value , fieldApi . state . meta ] ,
751+ [ children , fieldApi ] ,
665752 )
666753 return ( < > { jsxToDisplay } </ > ) as never
667754} ) satisfies FunctionComponent <
0 commit comments