Skip to content

Commit 22e0965

Browse files
committed
elimina el async set state
1 parent 2992344 commit 22e0965

2 files changed

Lines changed: 53 additions & 17 deletions

File tree

src/Form/index.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,51 @@ test('passes dom props to the form element', () => {
198198

199199
expect(container.querySelector('form').getAttribute('target')).toBe('/submit-here')
200200
})
201+
202+
test('resetState race condition: isUpdatingFromField.current blocks state reset', async () => {
203+
let form: FormRef = null
204+
const setState = jest.fn()
205+
206+
const {container, rerender} = render(
207+
<Form
208+
ref={handle => {
209+
form = handle
210+
}}
211+
state={{name: 'initial'}}
212+
onChange={setState}
213+
>
214+
<Field fieldName="name" type={DummyInput} />
215+
</Form>,
216+
)
217+
218+
// First, change the field value (this sets isUpdatingFromField.current = true)
219+
act(() => {
220+
fireEvent.change(container.querySelector('input'), {target: {value: 'changed'}})
221+
})
222+
223+
// Verify the change happened
224+
expect(form.getValue()).toEqual({name: 'changed'})
225+
226+
// Now simulate parent component updating props.state by rerendering with new state
227+
// This triggers the useDeepCompareEffect which calls resetState
228+
// However, resetState is blocked by isUpdatingFromField.current still being true
229+
act(() => {
230+
// to make sure all hooks and effects are executed
231+
rerender(
232+
<Form
233+
ref={handle => {
234+
form = handle
235+
}}
236+
state={{name: 'external'}}
237+
onChange={setState}
238+
>
239+
<Field fieldName="name" type={DummyInput} />
240+
</Form>,
241+
)
242+
})
243+
244+
// The state should be reset to {name: 'external'} from the new props.state
245+
// Currently this test will FAIL because of a race condition bug where
246+
// isUpdatingFromField.current prevents resetState from executing
247+
expect(form.getValue()).toEqual({name: 'external'})
248+
})

src/Form/index.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,12 @@ function Form(props: FormProps, ref: React.Ref<FormRef>) {
2727
const propsState = useMemo(() => cloneDeep(props.state || {}), [props.state])
2828
const [state, setState] = useState(propsState)
2929

30-
// Track if changes are coming from internal field updates
31-
const isUpdatingFromField = useRef(false)
32-
3330
const resetState = useCallback(() => {
3431
if (isEqual(propsState, state)) return
35-
// Only reset if we're not currently processing a field update
36-
if (!isUpdatingFromField.current) {
37-
// Use startTransition for form resets as they're typically not urgent
38-
startTransition(() => {
39-
setState(propsState)
40-
})
41-
}
32+
// Always allow reset, but use startTransition for async behavior
33+
startTransition(() => {
34+
setState(propsState)
35+
})
4236
}, [propsState, state])
4337

4438
// when the props state changes, we set the state to the new props.state
@@ -73,14 +67,8 @@ function Form(props: FormProps, ref: React.Ref<FormRef>) {
7367
}, [state, props.onChange, propsState])
7468

7569
const onChange = useCallback((fieldName: string, fieldValue: any) => {
76-
isUpdatingFromField.current = true
7770
setState(oldValue => {
78-
const newValue = getNewValue(oldValue, fieldName, fieldValue)
79-
// Reset the flag after state update
80-
Promise.resolve().then(() => {
81-
isUpdatingFromField.current = false
82-
})
83-
return newValue
71+
return getNewValue(oldValue, fieldName, fieldValue)
8472
})
8573
}, [])
8674

0 commit comments

Comments
 (0)