-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: Add support for automatic reset after React 19 form actions #8444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
import {useLayoutEffect} from './useLayoutEffect'; | ||
|
||
// Use the earliest effect type possible. useInsertionEffect runs during the mutation phase, | ||
// before all layout effects, but is available only in React 18 and later. | ||
const useEarlyEffect = React['useInsertionEffect'] ?? useLayoutEffect; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that we get the updated defaultValue in the onReset handler of useFormReset, which is called before layout effects run. This is only available in React 18 and later, so we fall back to useLayoutEffect. Shouldn't be a problem because the automatic form reset is React 19 only anyway.
Build successful! 🎉 |
## API Changes
react-aria-components/react-aria-components:CheckboxGroupState CheckboxGroupState {
addValue: (string) => void
commitValidation: () => void
+ defaultValue: readonly Array<string>
displayValidation: ValidationResult
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isSelected: (string) => boolean
realtimeValidation: ValidationResult
removeValue: (string) => void
resetValidation: () => void
setInvalid: (string, ValidationResult) => void
setValue: (Array<string>) => void
toggleValue: (string) => void
updateValidation: (ValidationResult) => void
value: readonly Array<string>
} /react-aria-components:ColorAreaState ColorAreaState {
channels: {
xChannel: ColorChannel
yChannel: ColorChannel
zChannel: ColorChannel
}
decrementX: (number) => void
decrementY: (number) => void
+ defaultValue: Color
getDisplayColor: () => Color
getThumbPosition: () => {
x: number
y: number
incrementX: (number) => void
incrementY: (number) => void
isDragging: boolean
setColorFromPoint: (number, number) => void
setDragging: (boolean) => void
setValue: (string | Color) => void
setXValue: (number) => void
setYValue: (number) => void
value: Color
xChannelPageStep: number
xChannelStep: number
xValue: number
yChannelPageStep: number
yChannelStep: number
yValue: number
} /react-aria-components:ColorFieldState ColorFieldState {
colorValue: Color | null
commit: () => void
commitValidation: () => void
decrement: () => void
decrementToMin: () => void
+ defaultColorValue: Color | null
displayValidation: ValidationResult
increment: () => void
incrementToMax: () => void
inputValue: string
realtimeValidation: ValidationResult
resetValidation: () => void
+ setColorValue: (Color | null) => void
setInputValue: (string) => void
updateValidation: (ValidationResult) => void
validate: (string) => boolean
} /react-aria-components:ColorSliderState ColorSliderState {
decrementThumb: (number, number) => void
+ defaultValues: Array<number>
focusedThumb: number | undefined
getDisplayColor: () => Color
getFormattedValue: (number) => string
getPercentValue: (number) => number
getThumbMinValue: (number) => number
getThumbPercent: (number) => number
getThumbValue: (number) => number
getThumbValueLabel: (number) => string
getValuePercent: (number) => number
incrementThumb: (number, number) => void
isDisabled: boolean
isDragging: boolean
isThumbDragging: (number) => boolean
isThumbEditable: (number) => boolean
orientation: Orientation
pageSize: number
setFocusedThumb: (number | undefined) => void
setThumbDragging: (number, boolean) => void
setThumbEditable: (number, boolean) => void
setThumbPercent: (number, number) => void
setThumbValue: (number, number) => void
setValue: (string | Color) => void
step: number
value: Color
values: Array<number>
} /react-aria-components:ColorWheelState ColorWheelState {
decrement: (number) => void
+ defaultValue: Color
getDisplayColor: () => Color
getThumbPosition: (number) => {
x: number
y: number
hue: number
increment: (number) => void
isDisabled: boolean
isDragging: boolean
pageStep: number
setDragging: (boolean) => void
setHue: (number) => void
setHueFromPoint: (number, number, number) => void
setValue: (string | Color) => void
step: number
value: Color
} /react-aria-components:ComboBoxState ComboBoxState <T> {
close: () => void
collection: Collection<Node<T>>
commit: () => void
commitValidation: () => void
+ defaultInputValue: string
+ defaultSelectedKey: Key | null
disabledKeys: Set<Key>
displayValidation: ValidationResult
focusStrategy: FocusStrategy | null
inputValue: string
isOpen: boolean
open: (FocusStrategy | null, MenuTriggerAction) => void
realtimeValidation: ValidationResult
resetValidation: () => void
revert: () => void
selectedItem: Node<T> | null
selectedKey: Key | null
selectionManager: SelectionManager
setFocused: (boolean) => void
setInputValue: (string) => void
setOpen: (boolean) => void
setSelectedKey: (Key | null) => void
toggle: (FocusStrategy | null, MenuTriggerAction) => void
updateValidation: (ValidationResult) => void
} /react-aria-components:DateFieldState DateFieldState {
calendar: Calendar
clearSegment: (SegmentType) => void
commitValidation: () => void
confirmPlaceholder: () => void
dateFormatter: DateFormatter
dateValue: Date
decrement: (SegmentType) => void
decrementPage: (SegmentType) => void
+ defaultValue: DateValue | null
displayValidation: ValidationResult
formatValue: (FieldOptions) => string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
incrementPage: (SegmentType) => void
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isRequired: boolean
maxGranularity: 'year' | 'month' | Granularity
realtimeValidation: ValidationResult
resetValidation: () => void
segments: Array<DateSegment>
setSegment: (SegmentType, number) => void
setValue: (DateValue | null) => void
updateValidation: (ValidationResult) => void
value: DateValue | null
} /react-aria-components:DatePickerState DatePickerState {
close: () => void
commitValidation: () => void
dateValue: DateValue | null
+ defaultValue: DateValue | null
displayValidation: ValidationResult
formatValue: (string, FieldOptions) => string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
isInvalid: boolean
isOpen: boolean
open: () => void
realtimeValidation: ValidationResult
resetValidation: () => void
setDateValue: (DateValue) => void
setOpen: (boolean) => void
setTimeValue: (TimeValue) => void
setValue: (DateValue | null) => void
timeValue: TimeValue | null
toggle: () => void
updateValidation: (ValidationResult) => void
value: DateValue | null
} /react-aria-components:DateRangePickerState DateRangePickerState {
close: () => void
commitValidation: () => void
dateRange: RangeValue<DateValue | null> | null
+ defaultValue: DateRange | null
displayValidation: ValidationResult
formatValue: (string, FieldOptions) => {
start: string
end: string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
hasTime: boolean
isInvalid: boolean
isOpen: boolean
open: () => void
realtimeValidation: ValidationResult
resetValidation: () => void
setDate: ('start' | 'end', DateValue | null) => void
setDateRange: (DateRange) => void
setDateTime: ('start' | 'end', DateValue | null) => void
setOpen: (boolean) => void
setTime: ('start' | 'end', TimeValue | null) => void
setTimeRange: (TimeRange) => void
setValue: (DateRange | null) => void
timeRange: RangeValue<TimeValue | null> | null
toggle: () => void
updateValidation: (ValidationResult) => void
value: RangeValue<DateValue | null>
} /react-aria-components:NumberFieldState NumberFieldState {
canDecrement: boolean
canIncrement: boolean
commit: () => void
commitValidation: () => void
decrement: () => void
decrementToMin: () => void
+ defaultNumberValue: number
displayValidation: ValidationResult
increment: () => void
incrementToMax: () => void
inputValue: string
minValue?: number
numberValue: number
realtimeValidation: ValidationResult
resetValidation: () => void
setInputValue: (string) => void
setNumberValue: (number) => void
updateValidation: (ValidationResult) => void
validate: (string) => boolean
} /react-aria-components:RadioGroupState RadioGroupState {
commitValidation: () => void
+ defaultSelectedValue: string | null
displayValidation: ValidationResult
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
lastFocusedValue: string | null
realtimeValidation: ValidationResult
resetValidation: () => void
selectedValue: string | null
setLastFocusedValue: (string | null) => void
setSelectedValue: (string | null) => void
updateValidation: (ValidationResult) => void
} /react-aria-components:SelectState SelectState <T> {
close: () => void
collection: Collection<Node<T>>
commitValidation: () => void
+ defaultSelectedKey: Key | null
disabledKeys: Set<Key>
displayValidation: ValidationResult
focusStrategy: FocusStrategy | null
isFocused: boolean
open: (FocusStrategy | null) => void
realtimeValidation: ValidationResult
resetValidation: () => void
selectedItem: Node<T> | null
selectedKey: Key | null
selectionManager: SelectionManager
setFocused: (boolean) => void
setOpen: (boolean) => void
setSelectedKey: (Key | null) => void
toggle: (FocusStrategy | null) => void
updateValidation: (ValidationResult) => void
} /react-aria-components:SliderState SliderState {
decrementThumb: (number, number) => void
+ defaultValues: Array<number>
focusedThumb: number | undefined
getFormattedValue: (number) => string
getPercentValue: (number) => number
getThumbMaxValue: (number) => number
getThumbPercent: (number) => number
getThumbValue: (number) => number
getThumbValueLabel: (number) => string
getValuePercent: (number) => number
incrementThumb: (number, number) => void
isDisabled: boolean
isThumbDragging: (number) => boolean
isThumbEditable: (number) => boolean
orientation: Orientation
pageSize: number
setFocusedThumb: (number | undefined) => void
setThumbDragging: (number, boolean) => void
setThumbEditable: (number, boolean) => void
setThumbPercent: (number, number) => void
setThumbValue: (number, number) => void
step: number
values: Array<number>
} /react-aria-components:TimeFieldState TimeFieldState {
calendar: Calendar
clearSegment: (SegmentType) => void
commitValidation: () => void
confirmPlaceholder: () => void
dateFormatter: DateFormatter
dateValue: Date
decrement: (SegmentType) => void
decrementPage: (SegmentType) => void
+ defaultValue: DateValue | null
displayValidation: ValidationResult
formatValue: (FieldOptions) => string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
incrementPage: (SegmentType) => void
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isRequired: boolean
maxGranularity: 'year' | 'month' | Granularity
realtimeValidation: ValidationResult
resetValidation: () => void
segments: Array<DateSegment>
setSegment: (SegmentType, number) => void
setValue: (DateValue | null) => void
timeValue: Time
updateValidation: (ValidationResult) => void
value: DateValue | null
} /react-aria-components:ToggleState ToggleState {
+ defaultSelected: boolean
isSelected: boolean
setSelected: (boolean) => void
toggle: () => void
} @react-stately/checkbox/@react-stately/checkbox:CheckboxGroupState CheckboxGroupState {
addValue: (string) => void
commitValidation: () => void
+ defaultValue: readonly Array<string>
displayValidation: ValidationResult
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isSelected: (string) => boolean
realtimeValidation: ValidationResult
removeValue: (string) => void
resetValidation: () => void
setInvalid: (string, ValidationResult) => void
setValue: (Array<string>) => void
toggleValue: (string) => void
updateValidation: (ValidationResult) => void
value: readonly Array<string>
} @react-stately/color/@react-stately/color:ColorAreaState ColorAreaState {
channels: {
xChannel: ColorChannel
yChannel: ColorChannel
zChannel: ColorChannel
}
decrementX: (number) => void
decrementY: (number) => void
+ defaultValue: Color
getDisplayColor: () => Color
getThumbPosition: () => {
x: number
y: number
incrementX: (number) => void
incrementY: (number) => void
isDragging: boolean
setColorFromPoint: (number, number) => void
setDragging: (boolean) => void
setValue: (string | Color) => void
setXValue: (number) => void
setYValue: (number) => void
value: Color
xChannelPageStep: number
xChannelStep: number
xValue: number
yChannelPageStep: number
yChannelStep: number
yValue: number
} /@react-stately/color:ColorSliderState ColorSliderState {
decrementThumb: (number, number) => void
+ defaultValues: Array<number>
focusedThumb: number | undefined
getDisplayColor: () => Color
getFormattedValue: (number) => string
getPercentValue: (number) => number
getThumbMinValue: (number) => number
getThumbPercent: (number) => number
getThumbValue: (number) => number
getThumbValueLabel: (number) => string
getValuePercent: (number) => number
incrementThumb: (number, number) => void
isDisabled: boolean
isDragging: boolean
isThumbDragging: (number) => boolean
isThumbEditable: (number) => boolean
orientation: Orientation
pageSize: number
setFocusedThumb: (number | undefined) => void
setThumbDragging: (number, boolean) => void
setThumbEditable: (number, boolean) => void
setThumbPercent: (number, number) => void
setThumbValue: (number, number) => void
setValue: (string | Color) => void
step: number
value: Color
values: Array<number>
} /@react-stately/color:ColorWheelState ColorWheelState {
decrement: (number) => void
+ defaultValue: Color
getDisplayColor: () => Color
getThumbPosition: (number) => {
x: number
y: number
hue: number
increment: (number) => void
isDisabled: boolean
isDragging: boolean
pageStep: number
setDragging: (boolean) => void
setHue: (number) => void
setHueFromPoint: (number, number, number) => void
setValue: (string | Color) => void
step: number
value: Color
} /@react-stately/color:ColorFieldState ColorFieldState {
colorValue: Color | null
commit: () => void
commitValidation: () => void
decrement: () => void
decrementToMin: () => void
+ defaultColorValue: Color | null
displayValidation: ValidationResult
increment: () => void
incrementToMax: () => void
inputValue: string
realtimeValidation: ValidationResult
resetValidation: () => void
+ setColorValue: (Color | null) => void
setInputValue: (string) => void
updateValidation: (ValidationResult) => void
validate: (string) => boolean
} /@react-stately/color:ColorChannelFieldState ColorChannelFieldState {
canDecrement: boolean
canIncrement: boolean
colorValue: Color
commit: () => void
commitValidation: () => void
decrement: () => void
decrementToMin: () => void
+ defaultColorValue: Color | null
+ defaultNumberValue: number
displayValidation: ValidationResult
increment: () => void
incrementToMax: () => void
inputValue: string
maxValue?: number
minValue?: number
numberValue: number
realtimeValidation: ValidationResult
resetValidation: () => void
+ setColorValue: (Color | null) => void
setInputValue: (string) => void
setNumberValue: (number) => void
updateValidation: (ValidationResult) => void
validate: (string) => boolean @react-stately/combobox/@react-stately/combobox:ComboBoxState ComboBoxState <T> {
close: () => void
collection: Collection<Node<T>>
commit: () => void
commitValidation: () => void
+ defaultInputValue: string
+ defaultSelectedKey: Key | null
disabledKeys: Set<Key>
displayValidation: ValidationResult
focusStrategy: FocusStrategy | null
inputValue: string
isOpen: boolean
open: (FocusStrategy | null, MenuTriggerAction) => void
realtimeValidation: ValidationResult
resetValidation: () => void
revert: () => void
selectedItem: Node<T> | null
selectedKey: Key | null
selectionManager: SelectionManager
setFocused: (boolean) => void
setInputValue: (string) => void
setOpen: (boolean) => void
setSelectedKey: (Key | null) => void
toggle: (FocusStrategy | null, MenuTriggerAction) => void
updateValidation: (ValidationResult) => void
} @react-stately/datepicker/@react-stately/datepicker:DateFieldState DateFieldState {
calendar: Calendar
clearSegment: (SegmentType) => void
commitValidation: () => void
confirmPlaceholder: () => void
dateFormatter: DateFormatter
dateValue: Date
decrement: (SegmentType) => void
decrementPage: (SegmentType) => void
+ defaultValue: DateValue | null
displayValidation: ValidationResult
formatValue: (FieldOptions) => string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
incrementPage: (SegmentType) => void
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isRequired: boolean
maxGranularity: 'year' | 'month' | Granularity
realtimeValidation: ValidationResult
resetValidation: () => void
segments: Array<DateSegment>
setSegment: (SegmentType, number) => void
setValue: (DateValue | null) => void
updateValidation: (ValidationResult) => void
value: DateValue | null
} /@react-stately/datepicker:DatePickerState DatePickerState {
close: () => void
commitValidation: () => void
dateValue: DateValue | null
+ defaultValue: DateValue | null
displayValidation: ValidationResult
formatValue: (string, FieldOptions) => string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
isInvalid: boolean
isOpen: boolean
open: () => void
realtimeValidation: ValidationResult
resetValidation: () => void
setDateValue: (DateValue) => void
setOpen: (boolean) => void
setTimeValue: (TimeValue) => void
setValue: (DateValue | null) => void
timeValue: TimeValue | null
toggle: () => void
updateValidation: (ValidationResult) => void
value: DateValue | null
} /@react-stately/datepicker:DateRangePickerState DateRangePickerState {
close: () => void
commitValidation: () => void
dateRange: RangeValue<DateValue | null> | null
+ defaultValue: DateRange | null
displayValidation: ValidationResult
formatValue: (string, FieldOptions) => {
start: string
end: string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
hasTime: boolean
isInvalid: boolean
isOpen: boolean
open: () => void
realtimeValidation: ValidationResult
resetValidation: () => void
setDate: ('start' | 'end', DateValue | null) => void
setDateRange: (DateRange) => void
setDateTime: ('start' | 'end', DateValue | null) => void
setOpen: (boolean) => void
setTime: ('start' | 'end', TimeValue | null) => void
setTimeRange: (TimeRange) => void
setValue: (DateRange | null) => void
timeRange: RangeValue<TimeValue | null> | null
toggle: () => void
updateValidation: (ValidationResult) => void
value: RangeValue<DateValue | null>
} /@react-stately/datepicker:TimeFieldState TimeFieldState {
calendar: Calendar
clearSegment: (SegmentType) => void
commitValidation: () => void
confirmPlaceholder: () => void
dateFormatter: DateFormatter
dateValue: Date
decrement: (SegmentType) => void
decrementPage: (SegmentType) => void
+ defaultValue: DateValue | null
displayValidation: ValidationResult
formatValue: (FieldOptions) => string
getDateFormatter: (string, FormatterOptions) => DateFormatter
granularity: Granularity
incrementPage: (SegmentType) => void
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
isRequired: boolean
maxGranularity: 'year' | 'month' | Granularity
realtimeValidation: ValidationResult
resetValidation: () => void
segments: Array<DateSegment>
setSegment: (SegmentType, number) => void
setValue: (DateValue | null) => void
timeValue: Time
updateValidation: (ValidationResult) => void
value: DateValue | null
} @react-stately/numberfield/@react-stately/numberfield:NumberFieldState NumberFieldState {
canDecrement: boolean
canIncrement: boolean
commit: () => void
commitValidation: () => void
decrement: () => void
decrementToMin: () => void
+ defaultNumberValue: number
displayValidation: ValidationResult
increment: () => void
incrementToMax: () => void
inputValue: string
minValue?: number
numberValue: number
realtimeValidation: ValidationResult
resetValidation: () => void
setInputValue: (string) => void
setNumberValue: (number) => void
updateValidation: (ValidationResult) => void
validate: (string) => boolean
} @react-stately/radio/@react-stately/radio:RadioGroupState RadioGroupState {
commitValidation: () => void
+ defaultSelectedValue: string | null
displayValidation: ValidationResult
isDisabled: boolean
isInvalid: boolean
isReadOnly: boolean
lastFocusedValue: string | null
realtimeValidation: ValidationResult
resetValidation: () => void
selectedValue: string | null
setLastFocusedValue: (string | null) => void
setSelectedValue: (string | null) => void
updateValidation: (ValidationResult) => void
} @react-stately/select/@react-stately/select:SelectState SelectState <T> {
close: () => void
collection: Collection<Node<T>>
commitValidation: () => void
+ defaultSelectedKey: Key | null
disabledKeys: Set<Key>
displayValidation: ValidationResult
focusStrategy: FocusStrategy | null
isFocused: boolean
open: (FocusStrategy | null) => void
realtimeValidation: ValidationResult
resetValidation: () => void
selectedItem: Node<T> | null
selectedKey: Key | null
selectionManager: SelectionManager
setFocused: (boolean) => void
setOpen: (boolean) => void
setSelectedKey: (Key | null) => void
toggle: (FocusStrategy | null) => void
updateValidation: (ValidationResult) => void
} @react-stately/slider/@react-stately/slider:SliderState SliderState {
decrementThumb: (number, number) => void
+ defaultValues: Array<number>
focusedThumb: number | undefined
getFormattedValue: (number) => string
getPercentValue: (number) => number
getThumbMaxValue: (number) => number
getThumbPercent: (number) => number
getThumbValue: (number) => number
getThumbValueLabel: (number) => string
getValuePercent: (number) => number
incrementThumb: (number, number) => void
isDisabled: boolean
isThumbDragging: (number) => boolean
isThumbEditable: (number) => boolean
orientation: Orientation
pageSize: number
setFocusedThumb: (number | undefined) => void
setThumbDragging: (number, boolean) => void
setThumbEditable: (number, boolean) => void
setThumbPercent: (number, number) => void
setThumbValue: (number, number) => void
step: number
values: Array<number>
} @react-stately/toggle/@react-stately/toggle:ToggleState ToggleState {
+ defaultSelected: boolean
isSelected: boolean
setSelected: (boolean) => void
toggle: () => void
} |
Fixes #6830
In React 19, when a
<form>
has anaction
prop that is a function, React will automatically reset the form after the action completes. This occurs after the commit phase of the render following the action so that thedefaultValue
can be updated based on the data returned by the server (but before layout effects run).Unlike the builtin
<input>
element, most of our components use controlled state internally, even when they appear uncontrolled to the user. Changing thedefaultValue
prop normally does nothing, but with native inputs resetting the form does use the currentdefaultValue
not the original value when the input first mounted. This PR updates all of our components to follow that behavior as well, by updating the internal state to match the currentdefaultValue
when the form is reset.This also fixes the validation errors being reset after an action, which occurred because the reset event would fire immediately after the validation errors returned by the server were set. Unfortunately react does not give us a way to detect this case, so this implements a best effort: if
form.reset()
is called programmatically outside a user event, we ignore it and do not clear the validation errors.