Skip to content

Commit 48cda0c

Browse files
authored
Merge pull request #1391 from rvsia/fixCondition
fix(condition): Remove useMemo and parse condition on change
2 parents 5d1c095 + bca6997 commit 48cda0c

File tree

2 files changed

+118
-67
lines changed

2 files changed

+118
-67
lines changed

packages/react-form-renderer/src/condition/condition.js

Lines changed: 61 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useReducer } from 'react';
1+
import { useCallback, useEffect, useMemo, useReducer } from 'react';
22
import PropTypes from 'prop-types';
33
import isEqual from 'lodash/isEqual';
44

@@ -33,76 +33,73 @@ export const reducer = (state, { type, sets }) => {
3333
}
3434
};
3535

36-
const Condition = React.memo(
37-
({ condition, children, values, field }) => {
38-
const formOptions = useFormApi();
39-
const dirty = formOptions.getState().dirty;
36+
const Condition = ({ condition, children, field }) => {
37+
const formOptions = useFormApi();
38+
const formState = formOptions.getState();
4039

41-
const [state, dispatch] = useReducer(reducer, {
42-
sets: [],
43-
initial: true,
44-
});
40+
const [state, dispatch] = useReducer(reducer, {
41+
sets: [],
42+
initial: true,
43+
});
4544

46-
// It is required to get the context state values from in order to get the latest state.
47-
// Using the trigger values can cause issues with the radio field as each input is registered separately to state and does not yield the actual field value.
48-
const conditionResult = parseCondition(condition, formOptions.getState().values, field);
45+
// It is required to get the context state values from in order to get the latest state.
46+
// Using the trigger values can cause issues with the radio field as each input is registered separately to state and does not yield the actual field value.
47+
const conditionResult = useMemo(() => parseCondition(condition, formState.values, field), [formState.values, condition, field]);
4948

50-
const setters = conditionResult.set ? [conditionResult.set] : conditionResult.sets;
49+
const setters = conditionResult.set ? [conditionResult.set] : conditionResult.sets;
5150

52-
useEffect(() => {
53-
if (!dirty) {
54-
dispatch({ type: 'formResetted' });
55-
}
56-
}, [dirty]);
51+
useEffect(() => {
52+
if (!formState.dirty) {
53+
dispatch({ type: 'formResetted' });
54+
}
55+
}, [formState.dirty]);
5756

58-
const setValue = useCallback((setter) => {
59-
Object.entries(setter).forEach(([name, value]) => {
60-
formOptions.change(name, value);
61-
});
62-
}, []);
63-
64-
useEffect(() => {
65-
if (setters && setters.length > 0 && (state.initial || !isEqual(setters, state.sets))) {
66-
setters.forEach((setter, index) => {
67-
if (setter && (state.initial || !isEqual(setter, state.sets[index]))) {
68-
setTimeout(() => {
69-
/**
70-
* We have to get the meta in the timetout to wait for state initialization
71-
*/
72-
const meta = formOptions.getFieldState(field.name);
73-
const isFormModified = Object.values(formOptions.getState().modified).some(Boolean);
74-
/**
75-
* Apply setter only
76-
* - field has no initial value
77-
* - form is modified
78-
* - when meta is false = field was unmounted before timeout, we finish the condition
79-
*/
80-
if (!meta || isFormModified || typeof meta.initial === 'undefined') {
81-
formOptions.batch(() => {
82-
if (typeof setter !== 'function') {
83-
setValue(setter);
57+
const setValue = useCallback((setter) => {
58+
Object.entries(setter).forEach(([name, value]) => {
59+
formOptions.change(name, value);
60+
});
61+
}, []);
62+
63+
useEffect(() => {
64+
if (setters && setters.length > 0 && (state.initial || !isEqual(setters, state.sets))) {
65+
setters.forEach((setter, index) => {
66+
if (setter && (state.initial || !isEqual(setter, state.sets[index]))) {
67+
setTimeout(() => {
68+
/**
69+
* We have to get the meta in the timetout to wait for state initialization
70+
*/
71+
const meta = formOptions.getFieldState(field.name);
72+
const isFormModified = Object.values(formOptions.getState().modified).some(Boolean);
73+
/**
74+
* Apply setter only
75+
* - field has no initial value
76+
* - form is modified
77+
* - when meta is false = field was unmounted before timeout, we finish the condition
78+
*/
79+
if (!meta || isFormModified || typeof meta.initial === 'undefined') {
80+
formOptions.batch(() => {
81+
if (typeof setter !== 'function') {
82+
setValue(setter);
83+
} else {
84+
const setterValue = setter(formOptions.getState(), formOptions.getFieldState);
85+
86+
if (setterValueCheck(setterValue)) {
87+
setValue(setterValue);
8488
} else {
85-
const setterValue = setter(formOptions.getState(), formOptions.getFieldState);
86-
87-
if (setterValueCheck(setterValue)) {
88-
setValue(setterValue);
89-
} else {
90-
console.error('Received invalid setterValue. Expected object, received: ', setterValue);
91-
}
89+
console.error('Received invalid setterValue. Expected object, received: ', setterValue);
9290
}
93-
});
94-
}
95-
});
96-
}
97-
});
98-
dispatch({ type: 'rememberSets', sets: setters });
99-
}
100-
}, [setters, state.initial]);
101-
102-
return conditionResult.visible ? children : null;
103-
},
104-
(a, b) => isEqual(a.values, b.values) && isEqual(a.condition, b.condition)
105-
);
91+
}
92+
});
93+
}
94+
});
95+
}
96+
});
97+
dispatch({ type: 'rememberSets', sets: setters });
98+
}
99+
}, [setters, state.initial]);
100+
101+
return conditionResult.visible ? children : null;
102+
};
106103

107104
const conditionProps = {
108105
when: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string), PropTypes.func]),

packages/react-form-renderer/src/tests/form-renderer/condition.test.js

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { act, render, screen, waitFor } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44

@@ -10,8 +10,8 @@ import FormRenderer from '../../form-renderer';
1010
import { reducer } from '../../condition';
1111

1212
const TextField = (props) => {
13-
const { input } = useFieldApi(props);
14-
return <input aria-label={input.name} {...input} />;
13+
const { input, placeholder } = useFieldApi(props);
14+
return <input aria-label={input.name} placeholder={placeholder} {...input} />;
1515
};
1616

1717
describe('condition test', () => {
@@ -715,6 +715,60 @@ describe('condition test', () => {
715715
});
716716
});
717717

718+
it('should be possible to change props to component with condition', async () => {
719+
const Dummy = () => {
720+
const [conditionalField, setConditionalField] = useState({
721+
component: componentTypes.TEXT_FIELD,
722+
name: 'field-2',
723+
condition: [
724+
{
725+
when: 'field-1',
726+
is: 'show',
727+
},
728+
],
729+
});
730+
731+
const onButton = () =>
732+
setConditionalField({
733+
...conditionalField,
734+
placeholder: 'Changed placeholder',
735+
});
736+
737+
return (
738+
<React.Fragment>
739+
<button type="button" onClick={onButton}>
740+
Change field
741+
</button>
742+
<FormRenderer
743+
{...initialProps}
744+
schema={{
745+
fields: [
746+
{
747+
component: componentTypes.TEXT_FIELD,
748+
name: 'field-1',
749+
},
750+
conditionalField,
751+
],
752+
}}
753+
/>
754+
</React.Fragment>
755+
);
756+
};
757+
758+
render(<Dummy />);
759+
760+
expect(screen.queryByLabelText('field-2')).not.toBeInTheDocument();
761+
762+
await userEvent.type(screen.getByLabelText('field-1'), 'show');
763+
764+
expect(screen.getByLabelText('field-2')).toBeInTheDocument();
765+
expect(screen.queryByPlaceholderText('Changed placeholder')).not.toBeInTheDocument();
766+
767+
await userEvent.click(screen.getByText('Change field'));
768+
769+
expect(screen.getByPlaceholderText('Changed placeholder')).toBeInTheDocument();
770+
});
771+
718772
describe('reducer', () => {
719773
it('returns default', () => {
720774
const initialState = {

0 commit comments

Comments
 (0)