Skip to content

Picker - migrate prop deprecation end #3737

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

Merged
merged 12 commits into from
Jun 15, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/getting-started/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,26 @@ Removed (use `PanView` instead)
Use the `backgroundColor` prop instead of `contentContainerStyle={{backgroundColor}}`
Fix card being transparent on Android
`onCollapseChanged` will now be called after the animation has ended (as was intended)

### Picker
The component was refactored to simplify its API and improve type safety.

Props migration:
- `migrate` - Removed (new API is default)
- `getItemValue` - Removed (value extraction is now automatic from `item.value`)
- `getItemLabel` - Removed (label extraction is now automatic from `item.label`)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe: "Use item.<X> to get <X>"

- `renderPicker` → `renderInput`
- `renderCustomModal` → `renderOverlay`
- `renderCustomDialogHeader` → `renderHeader`
- `pickerModalProps` → `customPickerProps.modalProps`
- `onShow` → `customPickerProps.modalProps.onShow`
- `children` → `items` (recommended)

Additional notes:
- The picker now only supports primitive values (string | number) instead of object-based values
- For individual items, use `getItemLabel` prop on `Picker.Item` instead of the removed picker-level `getItemLabel`
- Items structure remains the same: `{label: string, value: primitive}` format is unchanged
- The `getLabel` prop still works for custom label formatting of the selected value
- Better TypeScript support and improved performance

Check out the full API: https://wix.github.io/react-native-ui-lib/docs/components/form/Picker
26 changes: 9 additions & 17 deletions src/components/picker/PickerItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import View from '../view';
import TouchableOpacity from '../touchableOpacity';
import Image from '../image';
import Text from '../text';
import {getItemLabel, isItemSelected} from './PickerPresenter';
import {isItemSelected} from './PickerPresenter';
import PickerContext from './PickerContext';
import {PickerItemProps, PickerSingleValue} from './types';
import {PickerItemProps} from './types';

/**
* @description: Picker.Item, for configuring the Picker's selectable options
Expand All @@ -29,12 +29,8 @@ const PickerItem = (props: PickerItemProps) => {
testID
} = props;
const context = useContext(PickerContext);
const {migrate} = context;
const customRenderItem = props.renderItem || context.renderItem;
// @ts-expect-error TODO: fix after removing migrate prop completely
const itemValue = !migrate && typeof value === 'object' ? value?.value : value;
const isSelected = isItemSelected(itemValue, context.value);
const itemLabel = getItemLabel(label, value, props.getItemLabel || context.getItemLabel);
const isSelected = isItemSelected(value, context.value);
const selectedCounter = context.selectionLimit && _.isArray(context.value) && context.value?.length;
const accessibilityProps = {
accessibilityState: isSelected ? {selected: true} : undefined,
Expand Down Expand Up @@ -65,16 +61,12 @@ const PickerItem = (props: PickerItemProps) => {
const _onPress = useCallback(async (props: any) => {
// Using !(await onPress?.(item)) does not work properly when onPress is not sent
// We have to explicitly state `false` so a synchronous void (undefined) will still work as expected
if (onPress && await onPress(context.isMultiMode ? !isSelected : undefined, props) === false) {
if (onPress && (await onPress(context.isMultiMode ? !isSelected : undefined, props)) === false) {
return;
}
if (migrate) {
context.onPress(value);
} else {
// @ts-expect-error TODO: fix after removing migrate prop completely
context.onPress(typeof value === 'object' || context.isMultiMode ? value : ({value, label: itemLabel}) as PickerSingleValue);
}
}, [migrate, value, context.onPress, onPress]);
context.onPress(value);
},
[value, context.onPress, onPress]);

const onSelectedLayout = useCallback((...args: any[]) => {
_.invoke(context, 'onSelectedLayout', ...args);
Expand All @@ -84,7 +76,7 @@ const PickerItem = (props: PickerItemProps) => {
return (
<View style={styles.container} flex row spread centerV>
<Text numberOfLines={1} style={itemLabelStyle}>
{itemLabel}
{label}
</Text>
{selectedIndicator}
</View>
Expand All @@ -102,7 +94,7 @@ const PickerItem = (props: PickerItemProps) => {
customValue={props.customValue}
{...accessibilityProps}
>
{customRenderItem ? customRenderItem(value, {...props, isSelected, isItemDisabled}, itemLabel) : _renderItem()}
{customRenderItem ? customRenderItem(value, {...props, isSelected, isItemDisabled}, label) : _renderItem()}
</TouchableOpacity>
);
};
Expand Down
13 changes: 1 addition & 12 deletions src/components/picker/PickerPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ export function isItemSelected(childValue: PickerSingleValue, selectedValue?: Pi
if (Array.isArray(selectedValue)) {
isSelected =
_.find(selectedValue, v => {
// @ts-expect-error TODO: fix after removing migrate prop completely
return v === childValue || (typeof v === 'object' && v?.value === childValue);
return v === childValue;
}) !== undefined;
} else {
isSelected = childValue === selectedValue;
Expand All @@ -37,16 +36,6 @@ export function isItemSelected(childValue: PickerSingleValue, selectedValue?: Pi
// return _.invoke(props, 'getItemValue', props.value) || _.get(props.value, 'value');
// }

export function getItemLabel(label: string, value: PickerValue, getItemLabel?: PickerProps['getItemLabel']) {
if (_.isObject(value)) {
if (getItemLabel) {
return getItemLabel(value);
}
return _.get(value, 'label');
}
return label;
}

export function shouldFilterOut(searchValue: string, itemLabel?: string) {
return !_.includes(_.lowerCase(itemLabel), _.lowerCase(searchValue));
}
17 changes: 0 additions & 17 deletions src/components/picker/__tests__/PickerPresenter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,4 @@ describe('components/PickerPresenter', () => {
// expect(uut.getItemValue(itemProps)).toEqual('item-value');
// });
// });

describe('getItemLabel', () => {
it('should return item label when value is not an object', () => {
expect(uut.getItemLabel('label', 'value', undefined)).toEqual('label');
});

it('should return item label when value is an object', () => {
const itemProps = {value: {value: 'value', label: 'label'}};
expect(uut.getItemLabel(undefined, itemProps.value, undefined)).toEqual('label');
});

it('should return item label according to getLabel function ', () => {
const getLabel = itemValue => `${itemValue.value} - ${itemValue.label}`;
const itemProps = {value: {value: 'value', label: 'label'}, getLabel};
expect(uut.getItemLabel(undefined, itemProps.value, getLabel)).toEqual('value - label');
});
});
});
1 change: 0 additions & 1 deletion src/components/picker/api/picker.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/CustomPicker.gif?raw=true"
],
"props": [
{"name": "migrate", "type": "boolean", "description": "Temporary prop required for migration to Picker's new API"},
{"name": "value", "type": "string | number", "description": "Picker current value"},
{
"name": "onChange",
Expand Down
5 changes: 0 additions & 5 deletions src/components/picker/api/pickerItem.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@
{"name": "value", "type": "string | number", "description": "Item's value"},
{"name": "label", "type": "string", "description": "Item's label"},
{"name": "labelStyle", "type": "ViewStyle", "description": "Item's label style"},
{
"name": "getItemLabel",
"type": "(value: string | number) => string",
"description": "Custom function for the item label"
},
{"name": "isSelected", "type": "boolean", "description": "Is the item selected"},
{"name": "selectedIcon", "type": "string", "description": "Pass to change the selected icon"},
{"name": "selectedIconColor", "type": "ImageSource", "description": "Pass to change the selected icon color"},
Expand Down
8 changes: 4 additions & 4 deletions src/components/picker/helpers/usePickerLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import {PickerProps, PickerValue} from '../types';
interface UsePickerLabelProps
extends Pick<
PickerProps,
'value' | 'getLabel' | 'getItemLabel' | 'placeholder' | 'accessibilityLabel' | 'accessibilityHint'
'value' | 'getLabel' | 'placeholder' | 'accessibilityLabel' | 'accessibilityHint'
> {
items: {value: string | number; label: string}[] | null | undefined;
}

const usePickerLabel = (props: UsePickerLabelProps) => {
const {value, items, getLabel, getItemLabel, placeholder, accessibilityLabel, accessibilityHint} = props;
const {value, items, getLabel, placeholder, accessibilityLabel, accessibilityHint} = props;

const getLabelsFromArray = useCallback((value: PickerValue) => {
const itemsByValue = _.keyBy(items, 'value');
return _.flow(arr =>
_.map(arr, item => (_.isPlainObject(item) ? getItemLabel?.(item) || item?.label : itemsByValue[item]?.label)),
_.map(arr, item => (_.isPlainObject(item) ? item?.label : itemsByValue[item]?.label)),
arr => _.join(arr, ', '))(value);
}, [getItemLabel, items]);
}, [items]);

const _getLabel = useCallback((value: PickerValue) => {
if (_.isFunction(getLabel) && !_.isUndefined(getLabel(value))) {
Expand Down
19 changes: 2 additions & 17 deletions src/components/picker/helpers/usePickerMigrationWarnings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,15 @@ import {LogService} from '../../../services';
import {PickerProps} from '../types';

// TODO: Remove this whole file when migration is completed
type UsePickerMigrationWarnings = Pick<
PickerProps,
'children' | 'migrate' | 'getItemLabel' | 'getItemValue' | 'onShow'
>;
type UsePickerMigrationWarnings = Pick<PickerProps, 'children' | 'onShow'>;

const usePickerMigrationWarnings = (props: UsePickerMigrationWarnings) => {
const {children, migrate, getItemLabel, getItemValue, onShow} = props;
const {children, onShow} = props;
useEffect(() => {
if (children) {
LogService.warn(`UILib Picker will stop supporting the 'children' prop in the next major version, please pass 'items' prop instead`);
}

if (migrate) {
LogService.warn(`UILib Picker will stop supporting the 'migrate' prop in the next major version, please stop using it. The picker uses the new implementation by default.`);
}

if (getItemLabel) {
LogService.warn(`UILib Picker will stop supporting the 'getItemLabel' prop in the next major version, please pass the 'getItemLabel' prop to the specific item instead`);
}

if (getItemValue) {
LogService.warn(`UILib Picker will stop supporting the 'getItemValue' prop in the next major version, please stop using it. The value will be extract from 'items' prop instead`);
}

if (onShow) {
LogService.warn(`UILib Picker will stop supporting the 'onShow' prop in the next major version, please pass the 'onShow' prop from the 'pickerModalProps' instead`);
}
Expand Down
13 changes: 6 additions & 7 deletions src/components/picker/helpers/usePickerSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import {useCallback, useState, useMemo} from 'react';
import _ from 'lodash';
import {PickerProps} from '../types';
import {getItemLabel as getItemLabelPresenter, shouldFilterOut} from '../PickerPresenter';
import {shouldFilterOut} from '../PickerPresenter';

type UsePickerSearchProps = Pick<PickerProps, 'showSearch' | 'onSearchChange' | 'children' | 'getItemLabel' | 'items'>;
type UsePickerSearchProps = Pick<PickerProps, 'showSearch' | 'onSearchChange' | 'children' | 'items'>;

const usePickerSearch = (props: UsePickerSearchProps) => {
const {showSearch, onSearchChange, children, getItemLabel, items} = props;
const {showSearch, onSearchChange, children, items} = props;
const [searchValue, setSearchValue] = useState('');

const filterItems = useCallback((items: any) => {
if (showSearch && !_.isEmpty(searchValue)) {
return _.filter(items, item => {
const {label, value, getItemLabel: childGetItemLabel} = item.props || item;
const itemLabel = getItemLabelPresenter(label, value, childGetItemLabel || getItemLabel);
return !shouldFilterOut(searchValue, itemLabel);
const {label} = item.props || item;
return !shouldFilterOut(searchValue, label);
});
}
return items;
},
[showSearch, searchValue, getItemLabel]);
[showSearch, searchValue]);

const filteredItems = useMemo(() => {
return filterItems(children || items);
Expand Down
14 changes: 4 additions & 10 deletions src/components/picker/helpers/usePickerSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import _ from 'lodash';
import {PickerProps, PickerValue, PickerSingleValue, PickerMultiValue, PickerModes} from '../types';

interface UsePickerSelectionProps
extends Pick<PickerProps, 'migrate' | 'value' | 'onChange' | 'getItemValue' | 'topBarProps' | 'mode' | 'items'> {
extends Pick<PickerProps, 'value' | 'onChange' | 'topBarProps' | 'mode' | 'items'> {
pickerExpandableRef: RefObject<any>;
setSearchValue: (searchValue: string) => void;
}

const usePickerSelection = (props: UsePickerSelectionProps) => {
const {migrate, value, onChange, topBarProps, pickerExpandableRef, getItemValue, setSearchValue, mode, items} = props;
const {value, onChange, topBarProps, pickerExpandableRef, setSearchValue, mode, items} = props;
const [multiDraftValue, setMultiDraftValue] = useState(value as PickerMultiValue);
const [multiFinalValue, setMultiFinalValue] = useState(value as PickerMultiValue);

Expand All @@ -29,17 +29,11 @@ const usePickerSelection = (props: UsePickerSelectionProps) => {
[onChange]);

const toggleItemSelection = useCallback((item: PickerSingleValue) => {
let newValue;
const itemAsArray = [item];
if (!migrate) {
newValue = _.xorBy(multiDraftValue, itemAsArray, getItemValue || 'value');
} else {
newValue = _.xor(multiDraftValue, itemAsArray);
}

const newValue = _.xor(multiDraftValue, itemAsArray);
setMultiDraftValue(newValue);
},
[multiDraftValue, getItemValue]);
[multiDraftValue]);

const cancelSelect = useCallback(() => {
setSearchValue('');
Expand Down
28 changes: 6 additions & 22 deletions src/components/picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,9 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
listProps,
value,
getLabel,
getItemLabel,
getItemValue,
renderItem,
children,
useSafeArea,
// TODO: Remove migrate props and migrate code
migrate = true,
accessibilityLabel,
accessibilityHint,
items: propItems,
Expand All @@ -90,9 +86,6 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
const pickerExpandable = useRef<ExpandableOverlayMethods>(null);
const pickerRef = useImperativePickerHandle(ref, pickerExpandable);

// TODO: Remove this when migration is completed, starting of v8
// usePickerMigrationWarnings({children, migrate, getItemLabel, getItemValue});

useEffect(() => {
if (propItems) {
setItems(propItems);
Expand All @@ -103,7 +96,7 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
filteredItems,
setSearchValue,
onSearchChange: _onSearchChange
} = usePickerSearch({showSearch, onSearchChange, getItemLabel, children, items});
} = usePickerSearch({showSearch, onSearchChange, children, items});
const {
multiDraftValue,
onDoneSelecting,
Expand All @@ -113,11 +106,9 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
selectedCount,
toggleAllItemsSelection
} = usePickerSelection({
migrate,
value,
onChange,
pickerExpandableRef: pickerExpandable,
getItemValue,
topBarProps,
setSearchValue,
mode,
Expand All @@ -128,8 +119,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
if (propItems) {
return filteredItems.map((item: PickerItemProps) => ({
...item,
onPress: useWheelPicker && Constants.accessibility.isScreenReaderEnabled ?
() => onDoneSelecting(item.value) : undefined
onPress:
useWheelPicker && Constants.accessibility.isScreenReaderEnabled
? () => onDoneSelecting(item.value)
: undefined
}));
}
return filteredItems;
Expand All @@ -138,7 +131,6 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
const {label, accessibilityInfo} = usePickerLabel({
value,
items,
getItemLabel,
getLabel,
accessibilityLabel,
accessibilityHint,
Expand All @@ -163,15 +155,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
}, []);

const contextValue = useMemo(() => {
// @ts-expect-error cleanup after removing migrate prop
const pickerValue = !migrate && typeof value === 'object' && !_.isArray(value) ? value?.value : value;
return {
migrate,
value: mode === PickerModes.MULTI ? multiDraftValue : pickerValue,
value: mode === PickerModes.MULTI ? multiDraftValue : value,
onPress: mode === PickerModes.MULTI ? toggleItemSelection : onDoneSelecting,
isMultiMode: mode === PickerModes.MULTI,
getItemValue,
getItemLabel,
onSelectedLayout: onSelectedItemLayout,
renderItem,
selectionLimit,
Expand All @@ -180,13 +167,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
toggleAllItemsSelection
};
}, [
migrate,
mode,
value,
multiDraftValue,
renderItem,
getItemValue,
getItemLabel,
selectionLimit,
onSelectedItemLayout,
toggleItemSelection,
Expand Down
Loading