Skip to content

Commit 10e57d4

Browse files
devongovettsnowystingerLFDanLuktabors
authored
Remove function set state from useControlledState (#2304)
* Remove function set state from useControlledState * Fix lint and remaining issues * fix TS * Add a controlled story to test togglebutton * more controlled stories * fix checkbox group state * fix lint * add a controlled checkboxgroup story * Update packages/@react-spectrum/button/stories/ToggleButton.stories.tsx Co-authored-by: Kyle Taborski <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Daniel Lu <[email protected]> Co-authored-by: Kyle Taborski <[email protected]>
1 parent d7b2ca6 commit 10e57d4

File tree

15 files changed

+148
-98
lines changed

15 files changed

+148
-98
lines changed

packages/@react-aria/spinbutton/src/useSpinButton.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function useSpinButton(
5555
onIncrementToMax
5656
} = props;
5757
const formatMessage = useMessageFormatter(intlMessages);
58+
const propsRef = useRef(props);
59+
propsRef.current = props;
5860

5961
const clearAsync = () => clearTimeout(_async.current);
6062

@@ -136,7 +138,7 @@ export function useSpinButton(
136138
const onIncrementPressStart = useCallback(
137139
(initialStepDelay: number) => {
138140
clearAsync();
139-
onIncrement();
141+
propsRef.current.onIncrement();
140142
// Start spinning after initial delay
141143
_async.current = window.setTimeout(
142144
() => {
@@ -153,7 +155,7 @@ export function useSpinButton(
153155
const onDecrementPressStart = useCallback(
154156
(initialStepDelay: number) => {
155157
clearAsync();
156-
onDecrement();
158+
propsRef.current.onDecrement();
157159
// Start spinning after initial delay
158160
_async.current = window.setTimeout(
159161
() => {

packages/@react-spectrum/accordion/stories/Accordion.stories.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {Story as _Story, Meta} from '@storybook/react';
1414
import {Accordion, Item} from '../src';
15-
import React from 'react';
15+
import React, {useState} from 'react';
1616
import {SpectrumAccordionProps} from '@react-types/accordion';
1717

1818
type ItemType = {
@@ -62,10 +62,32 @@ const AccordionTemplate: Story<SpectrumAccordionProps<ItemType>> = (args) => (
6262
</Accordion>
6363
);
6464

65+
66+
const ControlledAccordionTemplate: Story<SpectrumAccordionProps<ItemType>> = (args) => {
67+
let [openKeys, setOpenKeys] = useState<Set<React.Key>>(new Set(['files']));
68+
return (
69+
<Accordion {...args} expandedKeys={openKeys} onExpandedChange={setOpenKeys} >
70+
<Item key="files" title="Your files">
71+
files
72+
</Item>
73+
<Item key="shared" title="Shared with you">
74+
shared
75+
</Item>
76+
<Item key="last" title="Last item">
77+
last
78+
</Item>
79+
</Accordion>
80+
);
81+
};
82+
6583
export const DefaultExpandedKeys = AccordionTemplate.bind({});
6684
DefaultExpandedKeys.storyName = 'defaultExpandedKeys: files';
6785
DefaultExpandedKeys.args = {defaultExpandedKeys: ['files']};
6886

87+
export const ControlledExpandedKeys = ControlledAccordionTemplate.bind({});
88+
ControlledExpandedKeys.storyName = 'controlled ExpandedKeys';
89+
ControlledExpandedKeys.args = {};
90+
6991
export const DisabledKeys = AccordionTemplate.bind({});
7092
DisabledKeys.storyName = 'disabledKeys: files, shared';
7193
DisabledKeys.args = {disabledKeys: ['files', 'shared']};

packages/@react-spectrum/button/stories/ToggleButton.stories.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {action} from '@storybook/addon-actions';
1414
import Add from '@spectrum-icons/workflow/Add';
1515
import {Flex, Text, View} from '@adobe/react-spectrum';
16-
import React from 'react';
16+
import React, {useState} from 'react';
1717
import {storiesOf} from '@storybook/react';
1818
import {ToggleButton} from '../';
1919

@@ -66,7 +66,10 @@ storiesOf('Button/ToggleButton', module)
6666
</Flex>
6767
</View>
6868
)
69-
);
69+
)
70+
.add('controlled state', () => (
71+
<ControlledToggleButton />
72+
));
7073

7174
function render(props = {}) {
7275
return (<Flex gap="size-100">
@@ -84,3 +87,14 @@ function render(props = {}) {
8487
</ToggleButton>
8588
</Flex>);
8689
}
90+
91+
function ControlledToggleButton() {
92+
let [selected, setSelected] = useState(false);
93+
return (
94+
<div>
95+
<ToggleButton isSelected={selected} onChange={setSelected}>Press Me</ToggleButton>
96+
<br />
97+
{selected ? 'true' : 'false'}
98+
</div>
99+
);
100+
}

packages/@react-spectrum/checkbox/stories/CheckboxGroup.stories.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {action} from '@storybook/addon-actions';
1414
import {Checkbox, CheckboxGroup} from '../';
15-
import React from 'react';
15+
import React, {useState} from 'react';
1616
import {SpectrumCheckboxGroupProps} from '@react-types/checkbox';
1717
import {storiesOf} from '@storybook/react';
1818

@@ -109,6 +109,10 @@ storiesOf('CheckboxGroup', module)
109109
.add(
110110
'form name',
111111
() => render({name: 'pets'})
112+
)
113+
.add(
114+
'controlled',
115+
() => <ControlledCheckboxGroup />
112116
);
113117

114118
function render(props: Omit<SpectrumCheckboxGroupProps, 'children'> = {}, checkboxProps: any[] = []) {
@@ -120,3 +124,14 @@ function render(props: Omit<SpectrumCheckboxGroupProps, 'children'> = {}, checkb
120124
</CheckboxGroup>
121125
);
122126
}
127+
128+
function ControlledCheckboxGroup() {
129+
let [checked, setChecked] = useState([]);
130+
return (
131+
<CheckboxGroup label="Pets" onChange={setChecked} value={checked}>
132+
<Checkbox value="dogs">Dogs</Checkbox>
133+
<Checkbox value="cats">Cats</Checkbox>
134+
<Checkbox value="dragons">Dragons</Checkbox>
135+
</CheckboxGroup>
136+
);
137+
}

packages/@react-spectrum/menu/stories/MenuTrigger.stories.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Cut from '@spectrum-icons/workflow/Cut';
2222
import {Item, Menu, MenuTrigger, Section} from '../';
2323
import {Keyboard, Text} from '@react-spectrum/text';
2424
import Paste from '@spectrum-icons/workflow/Paste';
25-
import React from 'react';
25+
import React, {useState} from 'react';
2626
import {storiesOf} from '@storybook/react';
2727

2828
let iconMap = {
@@ -527,6 +527,9 @@ storiesOf('MenuTrigger', module)
527527
<Item key="3">Three</Item>
528528
</Menu>
529529
)
530+
)
531+
.add('controlled isOpen',
532+
() => <ControlledOpeningMenuTrigger />
530533
);
531534

532535
let customMenuItem = (item) => {
@@ -567,3 +570,25 @@ let defaultMenu = (
567570
)}
568571
</Menu>
569572
);
573+
574+
function ControlledOpeningMenuTrigger() {
575+
let [isOpen, setIsOpen] = useState(false);
576+
return (
577+
<div style={{display: 'flex', width: 'auto', margin: '250px 0'}}>
578+
<MenuTrigger onOpenChange={setIsOpen} isOpen={isOpen}>
579+
<ActionButton
580+
onPress={action('press')}
581+
onPressStart={action('pressstart')}
582+
onPressEnd={action('pressend')}>
583+
Menu Button
584+
</ActionButton>
585+
<Menu onAction={action('onAction')}>
586+
<Item key="1">One</Item>
587+
<Item key="">Two</Item>
588+
<Item key="3">Three</Item>
589+
</Menu>
590+
</MenuTrigger>
591+
</div>
592+
);
593+
}
594+

packages/@react-spectrum/numberfield/test/NumberField.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1675,7 +1675,7 @@ describe('NumberField', function () {
16751675
expect(onChangeSpy).toHaveBeenCalledWith(10123);
16761676
});
16771677

1678-
it.skip.each`
1678+
it.each`
16791679
Name
16801680
${'NumberField controlled'}
16811681
`('$Name 10 is rendered and will change if the controlled version is implemented', () => {

packages/@react-stately/checkbox/src/useCheckboxGroupState.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,34 +64,27 @@ export function useCheckboxGroupState(props: CheckboxGroupProps = {}): CheckboxG
6464
if (props.isReadOnly || props.isDisabled) {
6565
return;
6666
}
67-
setValue(values => {
68-
if (!values.includes(value)) {
69-
return values.concat(value);
70-
}
71-
return values;
72-
});
67+
if (!selectedValues.includes(value)) {
68+
setValue(selectedValues.concat(value));
69+
}
7370
},
7471
removeValue(value) {
7572
if (props.isReadOnly || props.isDisabled) {
7673
return;
7774
}
78-
setValue(values => {
79-
if (values.includes(value)) {
80-
return values.filter(existingValue => existingValue !== value);
81-
}
82-
return values;
83-
});
75+
if (selectedValues.includes(value)) {
76+
setValue(selectedValues.filter(existingValue => existingValue !== value));
77+
}
8478
},
8579
toggleValue(value) {
8680
if (props.isReadOnly || props.isDisabled) {
8781
return;
8882
}
89-
setValue(values => {
90-
if (values.includes(value)) {
91-
return values.filter(existingValue => existingValue !== value);
92-
}
93-
return values.concat(value);
94-
});
83+
if (selectedValues.includes(value)) {
84+
setValue(selectedValues.filter(existingValue => existingValue !== value));
85+
} else {
86+
setValue(selectedValues.concat(value));
87+
}
9588
}
9689
};
9790

packages/@react-stately/checkbox/test/useCheckboxGroupState.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe('useCheckboxGroupState', () => {
228228
toggleValue('qwe');
229229
});
230230

231-
expect(container.textContent).toBe('foo, qwe');
231+
expect(container.textContent).toBe('foo');
232232
});
233233

234234
it('should not update state for readonly group', () => {

packages/@react-stately/color/src/useColorFieldState.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,7 @@ export function useColorFieldState(
7474
let [colorValue, setColorValue] = useControlledState<Color>(initialValue, initialDefaultValue, onChange);
7575
let [inputValue, setInputValue] = useState(() => (value || defaultValue) && colorValue ? colorValue.toString('hex') : '');
7676

77-
let safelySetColorValue = (newColor: Color | ((prevState: Color) => Color)) => {
78-
if (typeof newColor === 'function') {
79-
setColorValue((prev:Color) => {
80-
let resolved: Color = newColor(prev);
81-
if (!prev || !resolved) {
82-
return resolved;
83-
}
84-
if (resolved.toHexInt() !== prev.toHexInt()) {
85-
return resolved;
86-
}
87-
return prev;
88-
});
89-
return;
90-
}
77+
let safelySetColorValue = (newColor: Color) => {
9178
if (!colorValue || !newColor) {
9279
setColorValue(newColor);
9380
return;
@@ -138,30 +125,26 @@ export function useColorFieldState(
138125
};
139126

140127
let increment = () => {
141-
safelySetColorValue((prevColor: Color) => {
142-
let newValue = addColorValue(parsed.current, step);
143-
// if we've arrived at the same value that was previously in the state, the
144-
// input value should be updated to match
145-
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
146-
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
147-
if (newValue === prevColor) {
148-
setInputValue(newValue.toString('hex'));
149-
}
150-
return newValue;
151-
});
128+
let newValue = addColorValue(parsed.current, step);
129+
// if we've arrived at the same value that was previously in the state, the
130+
// input value should be updated to match
131+
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
132+
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
133+
if (newValue === colorValue) {
134+
setInputValue(newValue.toString('hex'));
135+
}
136+
safelySetColorValue(newValue);
152137
};
153138
let decrement = () => {
154-
safelySetColorValue((prevColor: Color) => {
155-
let newValue = addColorValue(parsed.current, -step);
156-
// if we've arrived at the same value that was previously in the state, the
157-
// input value should be updated to match
158-
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
159-
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
160-
if (newValue === prevColor) {
161-
setInputValue(newValue.toString('hex'));
162-
}
163-
return newValue;
164-
});
139+
let newValue = addColorValue(parsed.current, -step);
140+
// if we've arrived at the same value that was previously in the state, the
141+
// input value should be updated to match
142+
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
143+
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
144+
if (newValue === colorValue) {
145+
setInputValue(newValue.toString('hex'));
146+
}
147+
safelySetColorValue(newValue);
165148
};
166149
let incrementToMax = () => safelySetColorValue(MAX_COLOR);
167150
let decrementToMin = () => safelySetColorValue(MIN_COLOR);

packages/@react-stately/numberfield/src/useNumberFieldState.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,31 +168,27 @@ export function useNumberFieldState(
168168
};
169169

170170
let increment = () => {
171-
setNumberValue((previousValue) => {
172-
let newValue = safeNextStep('+', minValue);
173-
174-
// if we've arrived at the same value that was previously in the state, the
175-
// input value should be updated to match
176-
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
177-
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
178-
if (newValue === previousValue) {
179-
setInputValue(format(newValue));
180-
}
171+
let newValue = safeNextStep('+', minValue);
172+
173+
// if we've arrived at the same value that was previously in the state, the
174+
// input value should be updated to match
175+
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
176+
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
177+
if (newValue === numberValue) {
178+
setInputValue(format(newValue));
179+
}
181180

182-
return newValue;
183-
});
181+
setNumberValue(newValue);
184182
};
185183

186184
let decrement = () => {
187-
setNumberValue((previousValue) => {
188-
let newValue = safeNextStep('-', maxValue);
185+
let newValue = safeNextStep('-', maxValue);
189186

190-
if (newValue === previousValue) {
191-
setInputValue(format(newValue));
192-
}
187+
if (newValue === numberValue) {
188+
setInputValue(format(newValue));
189+
}
193190

194-
return newValue;
195-
});
191+
setNumberValue(newValue);
196192
};
197193

198194
let incrementToMax = () => {

0 commit comments

Comments
 (0)