Skip to content

Commit 35e5be8

Browse files
authored
fix(ui): client should add back default values for valid and passesCondition form field properties (#10709)
As a result of #9388, the `valid` and `passesCondition` properties no longer appear in form state. This leads to breaking logic if you were previously relying on these properties to have explicit values. To fix this, we simply perform the inverse on these properties before accepting them into client side form state. In the next major release, we can accept form state as it is received and instruct users to modify their logic as needed. Also comes with a small perf optimization, by keeping the old object reference of fields if they did not change when server form state comes back
1 parent 3985893 commit 35e5be8

File tree

4 files changed

+63
-24
lines changed

4 files changed

+63
-24
lines changed

packages/ui/src/forms/Form/fieldReducer.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,20 +284,39 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
284284
// ..
285285
// This is a performance enhancement for saving
286286
// large documents with hundreds of fields
287-
const newState = {}
287+
const newState: FormState = {}
288288

289-
Object.entries(action.state).forEach(([path, field]) => {
289+
for (const [path, newField] of Object.entries(action.state)) {
290290
const oldField = state[path]
291-
const newField = field
291+
292+
if (newField.valid !== false) {
293+
newField.valid = true
294+
}
295+
if (newField.passesCondition !== false) {
296+
newField.passesCondition = true
297+
}
292298

293299
if (!dequal(oldField, newField)) {
294300
newState[path] = newField
295301
} else if (oldField) {
296302
newState[path] = oldField
297303
}
298-
})
304+
}
305+
299306
return newState
300307
}
308+
309+
//TODO: Remove this in 4.0 - this is a temporary fix to prevent a breaking change
310+
if (action.sanitize) {
311+
for (const field of Object.values(action.state)) {
312+
if (field.valid !== false) {
313+
field.valid = true
314+
}
315+
if (field.passesCondition !== false) {
316+
field.passesCondition = true
317+
}
318+
}
319+
}
301320
// If we're not optimizing, just set the state to the new state
302321
return action.state
303322
}

packages/ui/src/forms/Form/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,12 @@ export const Form: React.FC<FormProps> = (props) => {
637637
useEffect(() => {
638638
if (initialState) {
639639
contextRef.current = { ...initContextState } as FormContextType
640-
dispatchFields({ type: 'REPLACE_STATE', optimize: false, state: initialState })
640+
dispatchFields({
641+
type: 'REPLACE_STATE',
642+
optimize: false,
643+
sanitize: true,
644+
state: initialState,
645+
})
641646
}
642647
}, [initialState, dispatchFields])
643648

packages/ui/src/forms/Form/mergeServerFormState.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,29 @@ export const mergeServerFormState = ({
2222
existingState,
2323
incomingState,
2424
}: Args): { changed: boolean; newState: FormState } => {
25-
const serverPropsToAccept = [
26-
'passesCondition',
27-
'valid',
28-
'errorMessage',
29-
'rows',
30-
'customComponents',
31-
'requiresRender',
32-
]
33-
34-
if (acceptValues) {
35-
serverPropsToAccept.push('value')
36-
}
37-
3825
let changed = false
3926

4027
const newState = {}
4128

4229
if (existingState) {
43-
Object.entries(existingState).forEach(([path, newFieldState]) => {
30+
const serverPropsToAccept = [
31+
'passesCondition',
32+
'valid',
33+
'errorMessage',
34+
'rows',
35+
'customComponents',
36+
'requiresRender',
37+
]
38+
39+
if (acceptValues) {
40+
serverPropsToAccept.push('value')
41+
}
42+
43+
for (const [path, newFieldState] of Object.entries(existingState)) {
4444
if (!incomingState[path]) {
45-
return
45+
continue
4646
}
47+
let fieldChanged = false
4748

4849
/**
4950
* Handle error paths
@@ -65,6 +66,7 @@ export const mergeServerFormState = ({
6566
if (incomingState[path]?.filterOptions || newFieldState.filterOptions) {
6667
if (!dequal(incomingState[path]?.filterOptions, newFieldState.filterOptions)) {
6768
changed = true
69+
fieldChanged = true
6870
newFieldState.filterOptions = incomingState[path].filterOptions
6971
}
7072
}
@@ -75,6 +77,7 @@ export const mergeServerFormState = ({
7577
serverPropsToAccept.forEach((prop) => {
7678
if (!dequal(incomingState[path]?.[prop], newFieldState[prop])) {
7779
changed = true
80+
fieldChanged = true
7881
if (!(prop in incomingState[path])) {
7982
// Regarding excluding the customComponents prop from being deleted: the incoming state might not have been rendered, as rendering components for every form onchange is expensive.
8083
// Thus, we simply re-use the initial render state
@@ -87,18 +90,25 @@ export const mergeServerFormState = ({
8790
}
8891
})
8992

93+
if (newFieldState.valid !== false) {
94+
newFieldState.valid = true
95+
}
96+
if (newFieldState.passesCondition !== false) {
97+
newFieldState.passesCondition = true
98+
}
99+
90100
// Conditions don't work if we don't memcopy the new state, as the object references would otherwise be the same
91-
newState[path] = { ...newFieldState }
92-
})
101+
newState[path] = fieldChanged ? { ...newFieldState } : newFieldState
102+
}
93103

94104
// Now loop over values that are part of incoming state but not part of existing state, and add them to the new state.
95105
// This can happen if a new array row was added. In our local state, we simply add out stubbed `array` and `array.[index].id` entries to the local form state.
96106
// However, all other array sub-fields are not added to the local state - those will be added by the server and may be incoming here.
97107

98-
for (const [path, newFieldState] of Object.entries(incomingState)) {
108+
for (const [path, field] of Object.entries(incomingState)) {
99109
if (!existingState[path]) {
100110
changed = true
101-
newState[path] = newFieldState
111+
newState[path] = field
102112
}
103113
}
104114
}

packages/ui/src/forms/Form/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export type Reset = (data: unknown) => Promise<void>
9191

9292
export type REPLACE_STATE = {
9393
optimize?: boolean
94+
/**
95+
* If `sanitize` is true, default values will be set for form field properties that are not present in the incoming state.
96+
* For example, `valid` will be set to true if it is not present in the incoming state.
97+
*/
98+
sanitize?: boolean
9499
state: FormState
95100
type: 'REPLACE_STATE'
96101
}

0 commit comments

Comments
 (0)